Repository: cekit/cekit

Issue:
Python library client should be used to request ODCS composes
**Describe the solution you'd like**
Currently we use the `odcs` CLI but we should use the Python library to have much cleaner integration.

**Additional context**
None


Discussion:
goldmann: Fixes #498.
goldmann: Fixes #410.

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a3ba614..d543756 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,6 +5,7 @@ jobs:
     machine:
       image: circleci/classic:201808-01
     steps:
+      - run: sudo apt-get install libkrb5-dev
       - checkout
       - run: docker version
       - run: docker info
diff --git a/Pipfile b/Pipfile
index 0f2658c..1d2b5b8 100644
--- a/Pipfile
+++ b/Pipfile
@@ -15,6 +15,7 @@ docker-squash = "*"
 click = "*"
 pylint = "*"
 autopep8 = "*"
+odcs = "*"
 
 [packages]
 pykwalify = ">=1.6.0"
diff --git a/cekit/builders/docker_builder.py b/cekit/builders/docker_builder.py
index 74347bf..888c45a 100644
--- a/cekit/builders/docker_builder.py
+++ b/cekit/builders/docker_builder.py
@@ -86,32 +86,41 @@ class DockerBuilder(Builder):
         docker_args['path'] = os.path.join(self.target, 'image')
         docker_args['pull'] = self.params.pull
         docker_args['rm'] = True
+        docker_args['decode'] = True
 
-        build_log = [""]
+        build_log = []
         docker_layer_ids = []
 
         try:
-            out = docker_client.build(**docker_args)
-            for line in out:
-                if b'stream' in line:
-                    line = yaml.safe_load(line)['stream']
-                elif b'status' in line:
-                    line = yaml.safe_load(line)['status']
-                elif b'errorDetail' in line:
-                    line = yaml.safe_load(line)['errorDetail']['message']
-                    raise CekitError("Image build failed: '%s'" % line)
-
-                if line != build_log[-1]:
-                    # this prevents poluting cekit log with dowloading/extracting msgs
-                    log_msg = ANSI_ESCAPE.sub('', line).strip()
-                    for msg in log_msg.split('\n'):
-                        LOGGER.info('Docker: %s' % msg)
-                    build_log.append(line)
-
-                    layer_id_match = re.search(r'^---> ([\w]{12})$', line.strip())
-
-                    if layer_id_match:
-                        docker_layer_ids.append(layer_id_match.group(1))
+            stream = docker_client.build(**docker_args)
+
+            for part in stream:
+                # In case an error is returned, log the message and fail the build
+                if 'errorDetail' in part:
+                    error_message = part.get('errorDetail', {}).get('message', '')
+                    raise CekitError("Image build failed: '{}'".format(error_message))
+                elif 'stream' in part:
+                    messages = part['stream']
+                else:
+                    # We actually expect only 'stream' here.
+                    # If there is something different, we ignore it.
+                    # It's safe to do so because if it would be an error, we would catch it
+                    # earlier. Ignored logs are related to fetching/pulling/extracing
+                    # of container images.
+                    continue
+
+                # This prevents poluting CEKit log with dowloading/extracting msgs
+                messages = ANSI_ESCAPE.sub('', messages).strip()
+
+                for message in messages.split('\n'):
+                    LOGGER.info('Docker: {}'.format(message))
+
+                build_log.append(messages)
+
+                layer_id_match = re.search(r'^---> ([\w]{12})$', messages)
+
+                if layer_id_match:
+                    docker_layer_ids.append(layer_id_match.group(1))
 
         except requests.ConnectionError as ex:
             exception_chain = traceback.format_exc()
@@ -119,8 +128,8 @@ class DockerBuilder(Builder):
 
             if 'PermissionError' in exception_chain:
                 message = "Unable to contact docker daemon. Is it correctly setup?\n" \
-                          "See https://developer.fedoraproject.org/tools/docker/docker-installation.html and " \
-                          "http://www.projectatomic.io/blog/2015/08/why-we-dont-let-non-root-users-run-docker-in-centos-fedora-or-rhel"
+                    "See https://developer.fedoraproject.org/tools/docker/docker-installation.html and " \
+                    "http://www.projectatomic.io/blog/2015/08/why-we-dont-let-non-root-users-run-docker-in-centos-fedora-or-rhel"
             elif 'FileNotFoundError' in exception_chain:
                 message = "Unable to contact docker daemon. Is it started?"
             else:
@@ -138,13 +147,13 @@ class DockerBuilder(Builder):
             msg = "Image build failed, see logs above."
             if len(docker_layer_ids) >= 2:
                 LOGGER.error("You can look inside the failed image by running "
-                             "'docker run --rm -ti %s bash'" % docker_layer_ids[-1])
+                             "'docker run --rm -ti {} bash'".format(docker_layer_ids[-1]))
             if "To enable Red Hat Subscription Management repositories:" in ' '.join(build_log) and \
                     not os.path.exists(os.path.join(self.target, 'image', 'repos')):
                 msg = "Image build failed with a yum error and you don't " \
-                      "have any yum repository configured, please check " \
-                      "your image/module descriptor for proper repository " \
-                      " definitions."
+                    "have any yum repository configured, please check " \
+                    "your image/module descriptor for proper repository " \
+                    " definitions."
             raise CekitError(msg, ex)
 
         return docker_layer_ids[-1]
diff --git a/cekit/generator/base.py b/cekit/generator/base.py
index 47d993d..3029050 100644
--- a/cekit/generator/base.py
+++ b/cekit/generator/base.py
@@ -3,11 +3,8 @@
 import logging
 import os
 import platform
-import subprocess
 import shutil
 
-import yaml
-
 from jinja2 import Environment, FileSystemLoader
 
 from cekit import tools
@@ -21,6 +18,14 @@ LOGGER = logging.getLogger('cekit')
 CONFIG = Config()
 
 
+try:
+    from odcs.client.odcs import ODCS, AuthMech
+    # Requests is a dependency of ODCS client, so this should be safe
+    import requests
+except ImportError:
+    pass
+
+
 class Generator(object):
     """
     This class process Image descriptor(self.image) and uses it to generate
@@ -51,6 +56,26 @@ class Generator(object):
 
         LOGGER.info("Initializing image descriptor...")
 
+    @staticmethod
+    def dependencies():
+        deps = {}
+
+        deps['odcs-client'] = {
+            'package': 'python3-odcs-client',
+            'library': 'odcs',
+            'rhel': {
+                'package': 'python2-odcs-client'
+            }
+        }
+
+        if CONFIG.get('common', 'redhat'):
+            deps['brew'] = {
+                'package': 'brewkoji',
+                'executable': '/usr/bin/brew'
+            }
+
+        return deps
+
     def init(self):
         """
         Initializes the image object.
@@ -72,10 +97,10 @@ class Generator(object):
 
     def generate(self, builder):
         self.copy_modules()
+        self.prepare_artifacts()
         self.prepare_repositories(builder)
         self.image.remove_none_keys()
         self.image.write(os.path.join(self.target, 'image.yaml'))
-        self.prepare_artifacts()
         self.render_dockerfile()
         self.render_help()
 
@@ -310,51 +335,64 @@ class Generator(object):
 
         repos = ' '.join(content_sets[arch])
 
-        try:
-            # ideally this will be API for ODCS, but there is no python3 package for ODCS
-            cmd = ['/usr/bin/odcs']
-            odcs_service_type = "Fedora"
+        odcs_service_type = "Fedora"
+        odcs_url = "https://odcs.fedoraproject.org"
+
+        if CONFIG.get('common', 'redhat'):
+            odcs_service_type = "Red Hat"
+            odcs_url = "https://odcs.engineering.redhat.com"
+
+        LOGGER.info("Using {} ODCS service to create composes".format(odcs_service_type))
+
+        flags = []
+
+        compose = self.image.get('osbs', {}).get(
+            'configuration', {}).get('container', {}).get('compose', {})
 
-            if CONFIG.get('common', 'redhat'):
-                odcs_service_type = "Red Hat"
-                cmd.append('--redhat')
+        if compose.get(Generator.ODCS_HIDDEN_REPOS_FLAG, False):
+            flags.append(Generator.ODCS_HIDDEN_REPOS_FLAG)
 
-            LOGGER.info("Using {} ODCS service to created composes".format(odcs_service_type))
+        odcs = ODCS(odcs_url, auth_mech=AuthMech.Kerberos)
+
+        LOGGER.debug(
+            "Requesting ODCS pulp compose for '{}' repositories with '{}' flags...".format(repos, flags))
+
+        try:
+            compose = odcs.new_compose(repos, 'pulp', flags=flags)
+        except requests.exceptions.HTTPError as ex:
+            if ex.response.status_code == 401:
+                LOGGER.error("You are not authorized to use {} ODCS service. Are you sure you have a valid Kerberos session?".format(
+                    odcs_service_type))
+            raise CekitError("Could not create ODCS compose", ex)
 
-            cmd.append('create')
+        compose_id = compose.get('id', None)
 
-            compose = self.image.get('osbs', {}).get(
-                'configuration', {}).get('container', {}).get('compose', {})
+        if not compose_id:
+            raise CekitError(
+                "Invalid response from ODCS service: no compose id found: {}".format(compose))
 
-            if compose.get(Generator.ODCS_HIDDEN_REPOS_FLAG, False):
-                cmd.extend(['--flag', Generator.ODCS_HIDDEN_REPOS_FLAG])
+        LOGGER.debug("Waiting for compose {} to finish...".format(compose_id))
 
-            cmd.extend(['pulp', repos])
+        compose = odcs.wait_for_compose(compose_id, timeout=600)
+        state = compose.get('state', None)
 
-            LOGGER.debug("Creating ODCS content set via '%s'" % " ".join(cmd))
+        if not state:
+            raise CekitError(
+                "Invalid response from ODCS service: no state found: {}".format(compose))
 
-            output = subprocess.check_output(cmd).decode()
-            normalized_output = '\n'.join(output.replace(" u'", " '")
-                                          .replace(' u"', ' "')
-                                          .split('\n')[1:])
+        # State 2 is "done"
+        if state != 2:
+            raise CekitError("Cannot create ODCS compose: '{}'".format(compose))
 
-            odcs_result = yaml.safe_load(normalized_output)
+        LOGGER.debug("Compose finished successfully")
 
-            if odcs_result['state'] != 2:
-                raise CekitError("Cannot create content set: '%s'"
-                                 % odcs_result['state_reason'])
+        repofile = compose.get('result_repofile', None)
 
-            repo_url = odcs_result['result_repofile']
-            return repo_url
+        if not repofile:
+            raise CekitError(
+                "Invalid response from ODCS service: no state_repofile key found: {}".format(compose))
 
-        except CekitError as ex:
-            raise ex
-        except OSError as ex:
-            raise CekitError("ODCS is not installed, please install 'odcs-client' package")
-        except subprocess.CalledProcessError as ex:
-            raise CekitError("Cannot create content set: '%s'" % ex.output)
-        except Exception as ex:
-            raise CekitError('Cannot create content set!', ex)
+        return repofile
 
     def _handle_repository(self, repo):
         """Process and prepares all v2 repositories.
diff --git a/cekit/generator/docker.py b/cekit/generator/docker.py
index 746394f..85b26ea 100644
--- a/cekit/generator/docker.py
+++ b/cekit/generator/docker.py
@@ -15,23 +15,6 @@ class DockerGenerator(Generator):
         super(DockerGenerator, self).__init__(descriptor_path, target, overrides)
         self._fetch_repos = True
 
-    @staticmethod
-    def dependencies():
-        deps = {}
-
-        if config.get('common', 'redhat'):
-            deps['odcs-client'] = {
-                'package': 'odcs-client',
-                'executable': '/usr/bin/odcs'
-            }
-
-            deps['brew'] = {
-                'package': 'brewkoji',
-                'executable': '/usr/bin/brew'
-            }
-
-        return deps
-
     def _prepare_repository_rpm(self, repo):
         # no special handling is needed here, everything is in template
         pass


Test patch:
diff --git a/tests/test_dockerfile.py b/tests/test_dockerfile.py
index 8a18d57..f319903 100644
--- a/tests/test_dockerfile.py
+++ b/tests/test_dockerfile.py
@@ -29,30 +29,6 @@ def print_test_name(value):
     return "\b"
 
 
-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'}"""
-
-
 @pytest.mark.parametrize('name, desc_part, exp_regex', [
     ('test_run_user',
      {'run': {'user': 1347, 'cmd': ['whoami']}}, r'.*USER 1347\n((#.*)?\n)*CMD.*'),
@@ -121,7 +97,9 @@ def test_dockerfile_rendering_tech_preview(tmpdir, mocker, desc_part, exp_regex)
 
 
 def test_dockerfile_docker_odcs_pulp(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.generator.docker.DockerGenerator.dependencies')
@@ -136,7 +114,9 @@ def test_dockerfile_docker_odcs_pulp(tmpdir, mocker):
 
 
 def test_dockerfile_docker_odcs_rpm(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.dependencies')
@@ -153,7 +133,9 @@ def test_dockerfile_docker_odcs_rpm(tmpdir, mocker):
 
 
 def test_dockerfile_docker_odcs_rpm_microdnf(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     target = str(tmpdir.mkdir('target'))
     desc_part = {'packages': {'manager': 'microdnf',
@@ -168,7 +150,9 @@ def test_dockerfile_docker_odcs_rpm_microdnf(tmpdir, mocker):
 
 
 def test_dockerfile_osbs_odcs_pulp(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.dependencies')
@@ -191,7 +175,9 @@ def test_dockerfile_osbs_odcs_pulp(tmpdir, mocker):
 
 
 def test_dockerfile_osbs_odcs_pulp_no_redhat(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.dependencies')
@@ -213,7 +199,9 @@ def test_dockerfile_osbs_odcs_pulp_no_redhat(tmpdir, mocker):
 
 def test_dockerfile_osbs_id_redhat_false(tmpdir, mocker):
     config.cfg['common']['redhat'] = True
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.dependencies')
@@ -230,7 +218,9 @@ def test_dockerfile_osbs_id_redhat_false(tmpdir, mocker):
 
 
 def test_dockerfile_osbs_url_only(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.prepare_dist_git')
     mocker.patch('cekit.builders.osbs.OSBSBuilder.dependencies')
@@ -250,7 +240,9 @@ def test_dockerfile_osbs_url_only(tmpdir, mocker):
 
 
 def test_dockerfile_osbs_odcs_rpm(tmpdir, mocker):
-    mocker.patch.object(subprocess, 'check_output', return_value=odcs_fake_resp)
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     mocker.patch.object(Repository, 'fetch')
     target = str(tmpdir.mkdir('target'))
 
diff --git a/tests/test_unit_builder_docker.py b/tests/test_unit_builder_docker.py
index bdd8378..a814d6f 100644
--- a/tests/test_unit_builder_docker.py
+++ b/tests/test_unit_builder_docker.py
@@ -26,86 +26,86 @@ config = Config()
 config.cfg['common'] = {'redhat': True}
 
 docker_success_output = [
-    b'{"stream":"Step 1/18 : FROM rhel7:7.5-released\\n"}\r\n',
-    b'{"stream":" ---\\u003e 7b875638cfd8\\n"}\r\n',
-    b'{"stream":"Step 2/18 : USER root\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e eeeb32196208\\n"}\r\n',
-    b'{"stream":"Step 3/18 : COPY repos/content_sets_odcs.repo /etc/yum.repos.d/\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e 5b8c17742206\\n"}\r\n',
-    b'{"stream":"Step 4/18 : RUN yum makecache\\n"}\r\n',
-    b'{"stream":" ---\\u003e Running in bbefb458e837\\n"}\r\n',
-    b'{"stream":"Loaded plugins: ovl, product-id, search-disabled-repos, subscription-manager\\n"}\r\n',
-    b'{"stream":"This system is not receiving updates. You can use subscription-manager on the host to register and assign subscriptions.\\n"}\r\n',
-    b'{"stream":"Metadata Cache Created\\n"}\r\n',
-    b'{"stream":" ---\\u003e 3c496e216ae4\\n"}\r\n',
-    b'{"stream":"Removing intermediate container bbefb458e837\\n"}\r\n',
-    b'{"stream":"Step 5/18 : COPY modules /tmp/scripts/\\n"}\r\n',
-    b'{"stream":" ---\\u003e d4128252660d\\n"}\r\n',
-    b'{"stream":"Removing intermediate container 14c16d02235a\\n"}\r\n',
-    b'{"stream":" ---\\u003e 382fd3d3b632\\n"}\r\n',
-    b'{"stream":"Removing intermediate container 7ce407d3f891\\n"}\r\n',
-    b'{"stream":"Step 16/18 : RUN rm /etc/yum.repos.d/content_sets_odcs.repo\\n"}\r\n',
-    b'{"stream":" ---\\u003e Running in e992e5580f21\\n"}\r\n',
-    b'{"stream":" ---\\u003e abf0d7a8ac3e\\n"}\r\n',
-    b'{"stream":"Removing intermediate container e992e5580f21\\n"}\r\n',
-    b'{"stream":"Step 17/18 : USER 185\\n"}\r\n',
-    b'{"stream":" ---\\u003e Running in 1148850ae2df\\n"}\r\n',
-    b'{"stream":" ---\\u003e 1a99d0e4c3cc\\n"}\r\n',
-    b'{"stream":"Removing intermediate container 1148850ae2df\\n"}\r\n',
-    b'{"stream":"Step 18/18 : WORKDIR /home/jboss\\n"}\r\n',
-    b'{"stream":" ---\\u003e 985573b8bb7b\\n"}\r\n',
-    b'{"stream":"Removing intermediate container 413ed29c9497\\n"}\r\n',
-    b'{"stream":"Successfully built 985573b8bb7b\\n"}\r\n'
+    {"stream": "Step 1/18 : FROM rhel7:7.5-released\n"},
+    {"stream": " ---> 7b875638cfd8\n"},
+    {"stream": "Step 2/18 : USER root\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> eeeb32196208\n"},
+    {"stream": "Step 3/18 : COPY repos/content_sets_odcs.repo /etc/yum.repos.d/\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> 5b8c17742206\n"},
+    {"stream": "Step 4/18 : RUN yum makecache\n"},
+    {"stream": " ---> Running in bbefb458e837\n"},
+    {"stream": "Loaded plugins: ovl, product-id, search-disabled-repos, subscription-manager\n"},
+    {"stream": "This system is not receiving updates. You can use subscription-manager on the host to register and assign subscriptions.\n"},
+    {"stream": "Metadata Cache Created\n"},
+    {"stream": " ---> 3c496e216ae4\n"},
+    {"stream": "Removing intermediate container bbefb458e837\n"},
+    {"stream": "Step 5/18 : COPY modules /tmp/scripts/\n"},
+    {"stream": " ---> d4128252660d\n"},
+    {"stream": "Removing intermediate container 14c16d02235a\n"},
+    {"stream": " ---> 382fd3d3b632\n"},
+    {"stream": "Removing intermediate container 7ce407d3f891\n"},
+    {"stream": "Step 16/18 : RUN rm /etc/yum.repos.d/content_sets_odcs.repo\n"},
+    {"stream": " ---> Running in e992e5580f21\n"},
+    {"stream": " ---> abf0d7a8ac3e\n"},
+    {"stream": "Removing intermediate container e992e5580f21\n"},
+    {"stream": "Step 17/18 : USER 185\n"},
+    {"stream": " ---> Running in 1148850ae2df\n"},
+    {"stream": " ---> 1a99d0e4c3cc\n"},
+    {"stream": "Removing intermediate container 1148850ae2df\n"},
+    {"stream": "Step 18/18 : WORKDIR /home/jboss\n"},
+    {"stream": " ---> 985573b8bb7b\n"},
+    {"stream": "Removing intermediate container 413ed29c9497\n"},
+    {"stream": "Successfully built 985573b8bb7b\n"}
 ]
 
 docker_fail_output = [
-    b'{"stream":"Step 1/159 : FROM ubi8-dev-preview/ubi-minimal\\n"}\r\n',
-    b'{"stream":" ---\\u003e c6070fd793df\\n"}\r\n',
-    b'{"stream":"Step 2/159 : USER root\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e 3841bbce3fdd\\n"}\r\n',
-    b'{"stream":"Step 3/159 : COPY modules /tmp/scripts/\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e 8f6d31270e03\\n"}\r\n',
-    b'{"stream":"Step 4/159 : COPY jboss-eap-7.2.1-patch.zip jboss-eap-7.2.zip jolokia-jvm-1.5.0.redhat-1-agent.jar txn-recovery-marker-jdbc-common-1.1.2.Final-redhat-00001.jar txn-recovery-marker-jdbc-hibernate5-1.1.2.Final-redhat-00001.jar openshift-ping-common-1.2.3.Final-redhat-1.jar openshift-ping-dns-1.2.3.Final-redhat-1.jar openshift-ping-kube-1.2.3.Final-redhat-1.jar oauth-20100527.jar activemq-rar-5.11.0.redhat-630371.rar jboss-logmanager-2.1.7.Final.jar rh-sso-7.2.2-eap7-adapter.zip rh-sso-7.2.2-saml-eap7-adapter.zip jmx_prometheus_javaagent-0.3.1.redhat-00006.jar /tmp/artifacts/\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e b901f2266584\\n"}\r\n',
-    b'{"stream":"Step 5/159 : USER root\\n"}\r\n',
-    b'{"stream":" ---\\u003e Using cache\\n"}\r\n',
-    b'{"stream":" ---\\u003e 81a88b63f47f\\n"}\r\n',
-    b'{"stream":"Step 6/159 : RUN microdnf --setopt=tsflags=nodocs install -y maven     \\u0026\\u0026 rpm -q maven\\n"}\r\n',
-    b'{"stream":" ---\\u003e Running in 4763fe199ffd\\n"}\r\n',
-    b'{"stream":"\\u001b[91m\\n(process:8): librhsm-WARNING **: 09:58:25.023: Found 0 entitlement certificates\\n\\u001b[0m"}\r\n',
-    b'{"stream":"\\u001b[91m\\n(process:8): librhsm-WARNING **: 09:58:25.024: Found 0 entitlement certificates\\n\\u001b[0m"}\r\n',
-    b'{"stream":"Downloading metadata...\\n"}\r\n',
-    b'{"stream":"Downloading metadata...\\n"}\r\n',
-    b'{"stream":"\\u001b[91merror: No package matches \'maven\'\\n\\u001b[0m"}\r\n',
-    b'{"errorDetail":{"code":1,"message":"The command \'/bin/sh -c microdnf --setopt=tsflags=nodocs install -y maven     \\u0026\\u0026 rpm -q maven\' returned a non-zero code: 1"},"error":"The command \'/bin/sh -c microdnf --setopt=tsflags=nodocs install -y maven     \\u0026\\u0026 rpm -q maven\' returned a non-zero code: 1"}\r\n'
+    {"stream": "Step 1/159 : FROM ubi8-dev-preview/ubi-minimal\n"},
+    {"stream": " ---> c6070fd793df\n"},
+    {"stream": "Step 2/159 : USER root\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> 3841bbce3fdd\n"},
+    {"stream": "Step 3/159 : COPY modules /tmp/scripts/\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> 8f6d31270e03\n"},
+    {"stream": "Step 4/159 : COPY jboss-eap-7.2.1-patch.zip jboss-eap-7.2.zip jolokia-jvm-1.5.0.redhat-1-agent.jar txn-recovery-marker-jdbc-common-1.1.2.Final-redhat-00001.jar txn-recovery-marker-jdbc-hibernate5-1.1.2.Final-redhat-00001.jar openshift-ping-common-1.2.3.Final-redhat-1.jar openshift-ping-dns-1.2.3.Final-redhat-1.jar openshift-ping-kube-1.2.3.Final-redhat-1.jar oauth-20100527.jar activemq-rar-5.11.0.redhat-630371.rar jboss-logmanager-2.1.7.Final.jar rh-sso-7.2.2-eap7-adapter.zip rh-sso-7.2.2-saml-eap7-adapter.zip jmx_prometheus_javaagent-0.3.1.redhat-00006.jar /tmp/artifacts/\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> b901f2266584\n"},
+    {"stream": "Step 5/159 : USER root\n"},
+    {"stream": " ---> Using cache\n"},
+    {"stream": " ---> 81a88b63f47f\n"},
+    {"stream": "Step 6/159 : RUN microdnf --setopt=tsflags=nodocs install -y maven     \u0026\u0026 rpm -q maven\n"},
+    {"stream": " ---> Running in 4763fe199ffd\n"},
+    {"stream": "\u001b[91m\n(process:8): librhsm-WARNING **: 09:58:25.023: Found 0 entitlement certificates\n\u001b[0m"},
+    {"stream": "\u001b[91m\n(process:8): librhsm-WARNING **: 09:58:25.024: Found 0 entitlement certificates\n\u001b[0m"},
+    {"stream": "Downloading metadata...\n"},
+    {"stream": "Downloading metadata...\n"},
+    {"stream": "\u001b[91merror: No package matches \'maven\'\n\u001b[0m"},
+    {"errorDetail": {"code": 1, "message": "The command \'/bin/sh -c microdnf --setopt=tsflags=nodocs install -y maven     \u0026\u0026 rpm -q maven\' returned a non-zero code: 1"},
+        "error": "The command \'/bin/sh -c microdnf --setopt=tsflags=nodocs install -y maven     \u0026\u0026 rpm -q maven\' returned a non-zero code: 1"}
 ]
 
 
 def test_docker_client_build(mocker, caplog):
     caplog.set_level(logging.DEBUG, logger="cekit")
 
-    builder = DockerBuilder(Map({'target': 'something'}), Map({'tags': ['foo', 'bar']}))
-
-    docker_client_class = mocker.patch('cekit.builders.docker_builder.APIClientClass')
     squash_class = mocker.patch('cekit.builders.docker_builder.Squash')
     squash = squash_class.return_value
+    docker_client_class = mocker.patch('cekit.builders.docker_builder.APIClientClass')
     docker_client = docker_client_class.return_value
     docker_client_build = mocker.patch.object(
         docker_client, 'build', return_value=docker_success_output)
 
+    builder = DockerBuilder(Map({'target': 'something'}), Map({'tags': ['foo', 'bar']}))
     builder.generator = Map({'image': {'from': 'FROM'}})
-
     builder.run()
 
     squash_class.assert_called_once_with(
         cleanup=True, docker=docker_client, from_layer="FROM", image="985573b8bb7b", log=logging.getLogger('cekit'))
     squash.run.assert_called_once_with()
-    docker_client_build.assert_called_once_with(path='something/image', pull=None, rm=True)
+    docker_client_build.assert_called_once_with(
+        decode=True, path='something/image', pull=None, rm=True)
     assert "Docker: This system is not receiving updates. You can use subscription-manager on the host to register and assign subscriptions." in caplog.text
     assert "Image built and available under following tags: foo, bar" in caplog.text
 
@@ -131,7 +131,8 @@ def test_docker_client_build_with_failure(mocker, caplog):
 
     squash_class.assert_not_called()
     squash.run.assert_not_called()
-    docker_client_build.assert_called_once_with(path='something/image', pull=None, rm=True)
+    docker_client_build.assert_called_once_with(
+        decode=True, path='something/image', pull=None, rm=True)
     assert "Docker: Step 3/159 : COPY modules /tmp/scripts/" in caplog.text
     assert "You can look inside the failed image by running 'docker run --rm -ti 81a88b63f47f bash'" in caplog.text
 
diff --git a/tests/test_unit_generator_docker.py b/tests/test_unit_generator_docker.py
index 82be142..69c863f 100644
--- a/tests/test_unit_generator_docker.py
+++ b/tests/test_unit_generator_docker.py
@@ -2,6 +2,7 @@
 
 # pylint: disable=protected-access
 
+import logging
 import os
 
 from contextlib import contextmanager
@@ -13,39 +14,56 @@ 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'}"""
+odcs_fake_resp = {
+    "arches": "x86_64 ppc64",
+    "flags": [
+        "no_deps"
+    ],
+    "id": 1,
+    "owner": "me",
+    "packages": "gofer-package",
+    "removed_by": None,
+    "result_repo": "https://odcs.fedoraproject.org/composes/latest-odcs-1-1/compose/Temporary",
+    "result_repofile": "http://hidden/Temporary/odcs-2019.repo",
+    "results": [
+        "repository"
+    ],
+    "sigkeys": "",
+    "source": "f26",
+    "source_type": 1,
+    "state": 2,
+    "state_name": "done",
+    "time_done": "2017-10-13T17:03:13Z",
+    "time_removed": "2017-10-14T17:00:00Z",
+    "time_submitted": "2017-10-13T16:59:51Z",
+    "time_to_expire": "2017-10-14T16:59:51Z"
+}
+
+odcs_fake_invalid_resp = {
+    "arches": "x86_64 ppc64",
+    "flags": [
+        "no_deps"
+    ],
+    "id": 1,
+    "owner": "me",
+    "packages": "gofer-package",
+    "removed_by": None,
+    "result_repo": "https://odcs.fedoraproject.org/composes/latest-odcs-1-1/compose/Temporary",
+    "result_repofile": "http://hidden/Temporary/odcs-2019.repo",
+    "results": [
+        "repository"
+    ],
+    "sigkeys": "",
+    "source": "f26",
+    "source_type": 1,
+    "state": 1,
+    "state_name": "wait",
+    "state_reason": "Compose failed",
+    "time_done": "2017-10-13T17:03:13Z",
+    "time_removed": "2017-10-14T17:00:00Z",
+    "time_submitted": "2017-10-13T16:59:51Z",
+    "time_to_expire": "2017-10-14T16:59:51Z"
+}
 
 
 @contextmanager
@@ -100,8 +118,10 @@ def test_prepare_content_sets_should_fail_when_cs_are_note_defined_for_current_p
 def test_prepare_content_sets_should_request_odcs(tmpdir, mocker):
     mocker.patch('cekit.generator.base.platform.machine',
                  return_value="current_platform")
-    mock_odcs = mocker.patch(
-        'cekit.generator.base.subprocess.check_output', return_value=odcs_fake_resp)
+    mock_odcs_new_compose = mocker.patch(
+        'odcs.client.odcs.ODCS.new_compose', return_value=odcs_fake_resp)
+    mock_odcs_wait_for_compose = mocker.patch(
+        'odcs.client.odcs.ODCS.wait_for_compose', return_value=odcs_fake_resp)
 
     with docker_generator(tmpdir) as generator:
         generator.image = {'osbs': {'configuration': {'container': {'compose': {}}}}}
@@ -110,14 +130,17 @@ def test_prepare_content_sets_should_request_odcs(tmpdir, mocker):
             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'])
+    mock_odcs_new_compose.assert_called_once_with('ca1 cs2', 'pulp', flags=[])
+    mock_odcs_wait_for_compose.assert_called_once_with(1, timeout=600)
 
 
 def test_prepare_content_sets_should_request_odcs_with_hidden_repos_flag(tmpdir, mocker):
     mocker.patch('cekit.generator.base.platform.machine',
                  return_value="current_platform")
-    mock_odcs = mocker.patch(
-        'cekit.generator.base.subprocess.check_output', return_value=odcs_fake_resp)
+    mock_odcs_new_compose = mocker.patch(
+        'odcs.client.odcs.ODCS.new_compose', return_value=odcs_fake_resp)
+    mock_odcs_wait_for_compose = mocker.patch(
+        'odcs.client.odcs.ODCS.wait_for_compose', return_value=odcs_fake_resp)
 
     with docker_generator(tmpdir) as generator:
         generator.image = {'osbs': {'configuration': {'container': {
@@ -127,41 +150,26 @@ def test_prepare_content_sets_should_request_odcs_with_hidden_repos_flag(tmpdir,
             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'])
+    mock_odcs_new_compose.assert_called_once_with(
+        'ca1 cs2', 'pulp', flags=['include_unpublished_pulp_repos'])
+    mock_odcs_wait_for_compose.assert_called_once_with(1, timeout=600)
 
 
 def test_prepare_content_sets_should_handle_incorrect_state(tmpdir, mocker):
     mocker.patch('cekit.generator.base.platform.machine',
                  return_value="current_platform")
-    mock_odcs = mocker.patch(
-        'cekit.generator.base.subprocess.check_output', return_value=odcs_fake_invalid_resp)
 
+    mock_odcs_new_compose = mocker.patch('odcs.client.odcs.ODCS.new_compose',
+                                         return_value=odcs_fake_invalid_resp)
+    mock_odcs_wait_for_compose = mocker.patch('odcs.client.odcs.ODCS.wait_for_compose',
+                                              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'"):
+            with pytest.raises(CekitError, match=r"Cannot create ODCS compose: '.*'"):
                 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.base.platform.machine',
-                 return_value="current_platform")
-    mock_odcs = mocker.patch(
-        'cekit.generator.base.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'])
+    mock_odcs_new_compose.assert_called_once_with('ca1 cs2', 'pulp', flags=[])
+    mock_odcs_wait_for_compose.assert_called_once_with(1, timeout=600)
diff --git a/tests/test_validate.py b/tests/test_validate.py
index 282dff9..6e4eb2c 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -116,6 +116,9 @@ def run_cekit_cs_overrides(image_dir, mocker, overrides_descriptor):
 def test_content_sets_file_container_file(tmpdir, mocker, caplog):
     # Do not try to validate dependencies while running tests, these are not necessary
     mocker.patch('cekit.generator.docker.DockerGenerator.dependencies').return_value({})
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     overrides_descriptor = {
         'schema_version': 1,
         'packages': {'content_sets_file': 'content_sets.yml'},
@@ -134,13 +137,18 @@ 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 '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Requesting ODCS pulp compose for 'aaa bbb' repositories with '[]' flags..." in caplog.text
+    assert "Waiting for compose 12 to finish..." in caplog.text
+    assert "Compose finished successfully" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
 def test_content_sets_file_container_embedded(tmpdir, mocker, caplog):
     # Do not try to validate dependencies while running tests, these are not necessary
     mocker.patch('cekit.generator.docker.DockerGenerator.dependencies').return_value({})
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     overrides_descriptor = {
         'schema_version': 1,
         'packages': {'content_sets_file': 'content_sets.yml'},
@@ -155,13 +163,18 @@ 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 '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Requesting ODCS pulp compose for 'aaa bbb' repositories with '[]' flags..." in caplog.text
+    assert "Waiting for compose 12 to finish..." in caplog.text
+    assert "Compose finished successfully" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
 def test_content_sets_embedded_container_embedded(tmpdir, mocker, caplog):
     # Do not try to validate dependencies while running tests, these are not necessary
     mocker.patch('cekit.generator.docker.DockerGenerator.dependencies').return_value({})
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
     overrides_descriptor = {
         'schema_version': 1,
         'packages': {'content_sets': {'x86_64': ['aaa', 'bbb']}},
@@ -171,13 +184,19 @@ 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 '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Requesting ODCS pulp compose for 'aaa bbb' repositories with '[]' flags..." in caplog.text
+    assert "Waiting for compose 12 to finish..." in caplog.text
+    assert "Compose finished successfully" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
 def test_content_sets_embedded_container_file(tmpdir, mocker, caplog):
     # Do not try to validate dependencies while running tests, these are not necessary
     mocker.patch('cekit.generator.docker.DockerGenerator.dependencies').return_value({})
+    mocker.patch('odcs.client.odcs.ODCS.new_compose', return_value={'id': 12})
+    mocker.patch('odcs.client.odcs.ODCS.wait_for_compose', return_value={
+                 'state': 2, 'result_repofile': 'url'})
+
     overrides_descriptor = {
         'schema_version': 1,
         'packages': {'content_sets': {'x86_64': ['aaa', 'bbb']}},
@@ -191,7 +210,9 @@ 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 '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Requesting ODCS pulp compose for 'aaa bbb' repositories with '[]' flags..." in caplog.text
+    assert "Waiting for compose 12 to finish..." in caplog.text
+    assert "Compose finished successfully" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
@@ -448,12 +469,6 @@ def test_override_add_module_and_packages_in_overrides(tmpdir, mocker):
                '--overrides', '{"artifacts": [{"name": "test", "path": "image.yaml"}] }',
                'docker'])
 
-    module_dir = os.path.join(image_dir,
-                              'target',
-                              'image',
-                              'modules',
-                              'foo')
-
     assert check_dockerfile(
         image_dir, 'RUN yum --setopt=tsflags=nodocs install -y package1 package2 \\')
     assert check_dockerfile(image_dir, 'RUN [ "bash", "-x", "/tmp/scripts/master/script_a" ]')