Container testing library

containers, testing, docker, fedora
pip install conu==1.0.0



PyPI PyPI - License PyPI - Status Codacy Badge Build Status

conu is a library which makes it easy to write tests for your containers and is handy when playing with containers inside your code. It defines an API to access and manipulate containers, images and provides more, very helpful functions.

conu is supported on python 3.6+ only.




conu is available on PyPI, so you can easily install it with pip:

$ pip install --user conu


If you are running Fedora, we have packaged conu in an RPM:

$ dnf install python3-conu

Please visit our documentation for more info on installation.

Docker container

You can try conu also in the container, but you have to:

  • mount docker socket
  • use --cap-add SYS_ADMIN for mounting containers/images
  • set --privileged option or turn off the SELinux to allow access to docker inside the container:
docker run -it --rm \
-v /var/run/docker.sock:/var/run/docker.sock:z \
--cap-add SYS_ADMIN \
--privileged \
usercont/conu:0.6.0 python3
>>> from conu import DockerBackend
>>> backend = DockerBackend()
11:52:13.022        INFO   conu has initiated, welcome to the party!
>>> image = backend.ImageClass('')
11:52:32.562       INFO   docker environment info: ...
>>> container = image.run_via_binary()
11:52:51.910          INFO   run container via binary in background

If you want to run custom source file, mount it to the container in the following way:

docker run -it --rm \
-v /var/run/docker.sock:/var/run/docker.sock:z \
-v $PWD/ \
--cap-add SYS_ADMIN \
--privileged \
usercont/conu:0.6.0 python3 /app/


Container images

  • load, pull, mount and remove container images
  • obtain low-level image metadata
  • check presence of files and directories inside a container image
  • read files inside an image
  • get selinux context of files in an image
  • extend image using s2i
  • check all packages in image are signed with a key
  • run image inside Kubernetes pod


  • kill, get logs, exec a command, mount, remove, start, stop, wait, run - via api or via binary
  • get low-level container metadata
  • shortcut methods for getting:
    • IPv4 and IPv6 addresses
    • PID of root process in the container
    • port mappings
    • container status
  • HTTP requests support
  • open a TCP connection with the service inside container
  • perform checks whether
    • the container is running
    • mapped ports are opened


  • easily create and delete a directory and set its options:
    • mode
    • ownership
    • selinux context
    • access control lists (facl)
  • port availability check
  • check SELinux status on host
  • run a command on host
  • easy random string generation
  • support for probes (execute a function in a separate process):
    • repeat until a condition is met
    • repeat N times
    • delay execution
    • delay between iterations


  • create/delete new namespace
  • create/delete Pod
  • create/delete Deployment
    • with parameters
    • from template
  • create/delete Service
  • shortcut methods for getting:
    • pod logs
    • pod IP
    • pod phase
    • pod condition
    • service IP
  • perform checks whether
    • pod is ready
    • all pods are ready for specific deployment


  • create new app using oc new-app command
    • deploy pure image into openshift
    • support building s2i images from remote repository
    • support building s2i images from local path
    • support creating new applications using OpenShift templates
  • push images to internal OpenShift registry
  • request service
  • waiting until service is ready
  • obtain logs from all pods
  • get status of application
  • check readiness of pods
  • cleanup objects of specific application in current namespace

Docker example

Let's look at a practical example:

$ cat examples/

import logging

from conu import DockerRunBuilder, DockerBackend

# our webserver will be accessible on this port
port = 8765

# we'll utilize this container image
image_name = ""
image_tag = "27"

# we'll run our container using docker engine
with DockerBackend(logging_level=logging.DEBUG) as backend:
    # the image will be pulled if it's not present
    image = backend.ImageClass(image_name, tag=image_tag)

    # the command to run in a container
    command = ["python3", "-m", "http.server", "--bind", "", "%d" % port]
    # let's run the container (in the background)
    container = image.run_via_binary(command=command)
        # we need to wait for the webserver to start serving
        # GET on /
        # this is standard `requests.Response`
        http_response = container.http_request(path="/", port=port)
        assert http_response.ok
        assert '<a href="etc/">etc/</a>' in http_response.content.decode("utf-8")
        # let's access /etc/passwd
        etc_passwd = container.http_request(path="/etc/passwd", port=port).content.decode("utf-8")
        assert 'root:x:0:0:root:/root:' in etc_passwd
        # we can also access it directly on disk and compare
        with container.mount() as fs:
            assert etc_passwd == fs.read_file("/etc/passwd")

Let's run it and look at the logs:

$ python3 examples/
13:32:17.668        INFO   conu has initiated, welcome to the party!
13:32:17.668        DEBUG  conu version: 0.1.0
13:32:17.669     INFO   initializing Directory(path=/tmp/shiny-kbjmsxgett)
13:32:17.669     DEBUG  changing permission bits of /tmp/shiny-kbjmsxgett to 0o700
13:32:17.669     INFO   initialized
13:32:17.676          INFO   run container via binary in background
13:32:17.676          DEBUG  docker command: ['docker', 'container', 'run', '-v', '/tmp/shiny-kbjmsxgett:/webroot', '-w', '/webroot', '-d', '--cidfile=/tmp/conu-b3jluxsc/conu-cbtbokqsedrtmiktfawbozgczdgxktmt', '-l', 'conu.test_artifact', 'sha256:9881e4229c9517b592980740ab2dfd8b5176adf7eb3be0f32b10a5dac5a3f12a', 'python3', '-m', 'http.server', '--bind', '', '8765']
13:32:17.676       DEBUG  command: ['docker', 'container', 'run', '-v', '/tmp/shiny-kbjmsxgett:/webroot', '-w', '/webroot', '-d', '--cidfile=/tmp/conu-b3jluxsc/conu-cbtbokqsedrtmiktfawbozgczdgxktmt', '-l', 'conu.test_artifact', 'sha256:9881e4229c9517b592980740ab2dfd8b5176adf7eb3be0f32b10a5dac5a3f12a', 'python3', '-m', 'http.server', '--bind', '', '8765']
13:32:18.131         DEBUG  starting probe
13:32:18.137         DEBUG  Running "<lambda>" with parameters: "{}": 0/10
13:32:18.133         DEBUG  first process started: pid=5812
13:32:18.141         DEBUG  pausing for 0.1 before next try
13:32:18.243         DEBUG  starting probe
13:32:18.244         DEBUG  first process started: pid=5828
13:32:18.245         DEBUG  pausing for 1 before next try
13:32:18.246         DEBUG  Running "functools.partial(<bound method DockerContainer.is_port_open of DockerContainer(, id=6a0530ab32c17858180c9c3867c17a2aaf3466c6dd17c329ab7a0cf9d991f626)>, 8765)" with parameters: "{}":      0/10
13:32:18.251       INFO   trying to open connection to
13:32:18.251       INFO   was connection successful? errno: 0
13:32:18.251       DEBUG  port is opened:
13:32:19.444     INFO   brace yourselves, removing '/tmp/shiny-kbjmsxgett'

The test passed! The logs should be easy to read, so you should have pretty good overview of what happened.


Use conu with minikube locally

If you want to test your images in Kubernetes locally, you will need to run kubernetes cluster on your host. We recommend to use minikube, for installation follow instructions in minikube github repository.

After that, run minikube like this:

$ minikube start

Kubernetes example

$ cat examples/
from conu.backend.k8s.backend import K8sBackend
from conu.backend.k8s.deployment import Deployment
from conu.utils import get_oc_api_token

# obtain API key from OpenShift cluster. If you are not using OpenShift cluster for kubernetes tests
# you need to replace `get_oc_api_token()` with your Bearer token. More information here:
api_key = get_oc_api_token()

with K8sBackend(api_key=api_key) as k8s_backend:

    namespace = k8s_backend.create_namespace()

    template = """
    apiVersion: apps/v1
    kind: Deployment
      name: hello-world
        app: hello-world
      replicas: 3
          app: hello-world
            app: hello-world
          - name: hello-openshift
            image: openshift/hello-openshift

    test_deployment = Deployment(namespace=namespace, from_template=template,

        assert test_deployment.all_pods_ready()

Let's run it and look at the logs:

$ python3 examples/
13:23:09.479        INFO   conu has initiated, welcome to the party!
13:23:09.523        INFO   Creating namespace: namespace-m4cz
13:23:14.557        INFO   Namespace is ready!
13:23:19.562     INFO   Creating Deployment hello-world in namespace: namespace-m4cz
13:23:27.625     INFO   All pods are ready for deployment hello-world in namespace: namespace-m4cz
13:23:28.620     INFO   Deleting Deployment hello-world in namespace: namespace-m4cz
13:23:28.654        INFO   Deleting namespace: namespace-m4cz


Use conu for testing locally

If you want to test your images in OpenShift locally, you need to run OpenShift cluster on your host. You can install it by following instructions in OpenShift origin or minishift github repositories.

After that, you may need to setup cluster, here is example setup:

oc cluster up
oc login -u system:admin
oadm policy add-role-to-user system:registry developer
oadm policy add-role-to-user admin developer
oadm policy add-role-to-user system:image-builder developer
oadm policy add-cluster-role-to-user cluster-reader developer
oadm policy add-cluster-role-to-user admin developer
oadm policy add-cluster-role-to-user cluster-admin developer
oc login -u developer -p developer

For more information, why do you need to grant all these rights to user see accessing registry

OpenShift example

$ cat examples/oepnshift/
import logging

from conu.backend.origin.backend import OpenshiftBackend
from conu.backend.docker.backend import DockerBackend
from conu.utils import get_oc_api_token

api_key = get_oc_api_token()
with OpenshiftBackend(api_key=api_key, logging_level=logging.DEBUG) as openshift_backend:
    with DockerBackend(logging_level=logging.DEBUG) as backend:
        # builder image
        python_image = backend.ImageClass("centos/python-36-centos7")

        # docker login inside OpenShift internal registry

        # create new app from remote source in OpenShift cluster
        app_name = openshift_backend.new_app(python_image,

            # wait until service is ready to accept requests
                expected_output='Welcome to your Django application on OpenShift',

Let's run it and look at the logs:

$ python3 examples/openshift/
13:29:38.231        INFO   conu has initiated, welcome to the party!
13:29:38.231        DEBUG  conu version: 0.5.0
13:29:38.256        INFO   conu has initiated, welcome to the party!
13:29:38.256        DEBUG  conu version: 0.5.0
13:29:38.314       INFO   docker environment info: 'Client:\n Version:         1.13.1\n API version:     1.26\n Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64\n Go version:      go1.10.3\n Git commit:      1556cce-unsupported\n Built:           Wed Aug  1 17:21:17 2018\n OS/Arch:         linux/amd64\n\nServer:\n Version:         1.13.1\n API version:     1.26 (minimum version 1.12)\n Package version: docker-1.13.1-74.git6e3bb8e.el7.centos.x86_64\n Go version:      go1.9.4\n Git commit:      6e3bb8e/1.13.1\n Built:           Tue Aug 21 15:23:37 2018\n OS/Arch:         linux/amd64\n Experimental:    false\n'
13:29:38.326        INFO   conu has initiated, welcome to the party!
13:29:38.584        INFO   conu has initiated, welcome to the party!
13:29:38.656        INFO   Login to succeed
13:29:38.656        INFO   conu has initiated, welcome to the party!
13:29:38.673          INFO   The push refers to a repository []
13:29:38.689          INFO   Preparing
13:29:38.689          INFO   Preparing
13:29:38.689          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.690          INFO   Preparing
13:29:38.700          INFO   Waiting
13:29:38.701          INFO   Waiting
13:29:38.701          INFO   Waiting
13:29:38.701          INFO   Waiting
13:29:38.747          INFO   Layer already exists
13:29:38.747          INFO   Layer already exists
13:29:38.753          INFO   Layer already exists
13:29:38.754          INFO   Layer already exists
13:29:38.772          INFO   Layer already exists
13:29:38.807          INFO   Layer already exists
13:29:38.807          INFO   Layer already exists
13:29:38.807          INFO   Layer already exists
13:29:38.807          INFO   Layer already exists
13:29:39.065          INFO   latest: digest: sha256:51cf14c1d1491c5ab0e902c52740c22d4fff52f95111b97d195d12325a426350 size: 2210
13:29:39.065        INFO   Creating new app in project myproject
13:29:39.558        INFO   Waiting for service to get ready
13:30:06.768        INFO   Connection to service established and return expected output!
13:30:07.729        INFO   Deleting app
13:30:09.504        INFO   deploymentconfig "app-u4ow" deleted
13:30:09.504        INFO   buildconfig "app-u4ow" deleted
13:30:09.504        INFO   imagestream "app-u4ow" deleted
13:30:09.504        INFO   pod "app-u4ow-1-vltwq" deleted
13:30:09.504        INFO   service "app-u4ow" deleted

Real examples


For more info see our documentation at

How to release conu

We are using awesome release-bot for new conu releases. If you want to make new release:

  • create new issue with title x.y.z release and wait for release bot to create new PR.
  • polish and merge if tests are passing.
  • Sit down, relax and watch how release bot is doing the hard work.