Browse Source

Add docker-compose and base dockerfile for development (#4482)

* Add docker-compose and base dockerfile for development

* Change to base jessie docker image and setup pyenv

* Add in aliases for pyenv, fix entrypoint for docker

* Update dockerfile to be non-root and fix encoding problems with tox

* Add convenience method to get redis connection for tests

* Add pypy to install commands

* Force worker to pick up broker url from environment

* Move pypy comment to above apt-get install, default python to 3.6, add in flag to prevent bytecode

* Add documentation

* Fix links

* Update docs

* Add to contributors

* Address feedback: improve documentation, separate dockerfile into scripts, remove redundancy in pyenv setup, add in .env file

* Setup pyenv environments correctly in dockerfile

* Update capitalization

* Change CELERY_USER to ARG in dockerfile and pass build argument in build

* Change default worker loglevel to debug in docker-compose
Chris Mitchell 7 years ago
parent
commit
59d140082f

+ 49 - 0
CONTRIBUTING.rst

@@ -448,6 +448,55 @@ fetch and checkout a remote branch like this::
     https://notes.envato.com/developers/rebasing-merge-commits-in-git/
     https://notes.envato.com/developers/rebasing-merge-commits-in-git/
 .. _`Rebase`: https://help.github.com/rebase/
 .. _`Rebase`: https://help.github.com/rebase/
 
 
+.. _contributing-docker-development:
+
+Developing and Testing with Docker
+----------------------------------
+
+Because of the many components of Celery, such as a broker and backend,
+`Docker`_ and `docker-compose`_ can be utilized to greatly simplify the
+development and testing cycle. The Docker configuration here requires a
+Docker version of at least 17.09.
+
+The Docker components can be found within the :file:`docker/` folder and the
+Docker image can be built via:
+
+.. code-block:: console
+
+    $ docker-compose build celery
+
+and run via:
+
+.. code-block:: console
+
+    $ docker-compose run --rm celery <command>
+
+where <command> is a command to execute in a Docker container. The `--rm` flag
+indicates that the container should be removed after it is exited and is useful
+to prevent accumulation of unwanted containers.
+
+Some useful commands to run:
+
+* ``bash``
+
+    To enter the Docker container like a normal shell
+
+* ``make test``
+
+    To run the test suite
+
+* ``tox``
+
+    To run tox and test against a variety of configurations
+
+By default, docker-compose will mount the Celery and test folders in the Docker
+container, allowing code changes and testing to be immediately visible inside
+the Docker container. Environment variables, such as the broker and backend to
+use are also defined in the :file:`docker/docker-compose.yml` file.
+
+.. _`Docker`: https://www.docker.com/
+.. _`docker-compose`: https://docs.docker.com/compose/
+
 .. _contributing-testing:
 .. _contributing-testing:
 
 
 Running the unit test suite
 Running the unit test suite

+ 1 - 0
CONTRIBUTORS.txt

@@ -257,3 +257,4 @@ Mikhail Wolfson, 2017/12/11
 Alex Garel, 2018/01/04
 Alex Garel, 2018/01/04
 Régis Behmo 2018/01/20
 Régis Behmo 2018/01/20
 Igor Kasianov, 2018/01/20
 Igor Kasianov, 2018/01/20
+Chris Mitchell, 2018/02/27

+ 1 - 1
celery/contrib/testing/worker.py

@@ -102,7 +102,7 @@ def _start_worker_thread(app,
     setup_app_for_worker(app, loglevel, logfile)
     setup_app_for_worker(app, loglevel, logfile)
     assert 'celery.ping' in app.tasks
     assert 'celery.ping' in app.tasks
     # Make sure we can connect to the broker
     # Make sure we can connect to the broker
-    with app.connection() as conn:
+    with app.connection(hostname=os.environ.get('TEST_BROKER')) as conn:
         conn.default_channel.queue_declare
         conn.default_channel.queue_declare
 
 
     worker = WorkController(
     worker = WorkController(

+ 1 - 0
docker/.env

@@ -0,0 +1 @@
+CELERY_USER=developer

+ 78 - 0
docker/Dockerfile

@@ -0,0 +1,78 @@
+FROM debian:jessie
+
+ENV PYTHONIOENCODING UTF-8
+
+# Pypy is installed from a package manager because it takes so long to build.
+RUN apt-get update && apt-get install -y \
+    build-essential \
+    curl \
+    git \
+    libbz2-dev \
+    libcurl4-openssl-dev \
+    libmemcached-dev \
+    libncurses5-dev \
+    libreadline-dev \
+    libsqlite3-dev \
+    libssl-dev \
+    pkg-config \
+    pypy \
+    wget \
+    zlib1g-dev
+
+# Setup variables. Even though changing these may cause unnecessary invalidation of
+# unrelated elements, grouping them together makes the Dockerfile read better.
+ENV PROVISIONING /provisioning
+
+# This is provisioned from .env
+ARG CELERY_USER=developer
+
+# Check for mandatory build arguments
+RUN : "${CELERY_USER:?CELERY_USER build argument needs to be set and non-empty.}"
+
+ENV HOME /home/$CELERY_USER
+ENV PATH="$HOME/.pyenv/bin:$PATH"
+
+# Copy and run setup scripts
+WORKDIR $PROVISIONING
+COPY docker/scripts/install-couchbase.sh .
+# Scripts will lose thier executable flags on copy. To avoid the extra instructions
+# we call the shell directly.
+RUN sh install-couchbase.sh
+COPY docker/scripts/create-linux-user.sh .
+RUN sh create-linux-user.sh
+
+# Swap to the celery user so packages and celery are not installed as root.
+USER $CELERY_USER
+
+COPY docker/scripts/install-pyenv.sh .
+RUN sh install-pyenv.sh
+
+# Install celery
+WORKDIR $HOME
+COPY --chown=1000:1000 requirements $HOME/requirements
+COPY --chown=1000:1000 docker/entrypoint /entrypoint
+RUN chmod gu+x /entrypoint
+
+# Define the local pyenvs
+RUN pyenv local python2.7 python3.4 python3.5 python3.6
+
+# Setup one celery environment for basic development use
+RUN pyenv exec pip install \
+  -r requirements/default.txt \
+  -r requirements/pkgutils.txt \
+  -r requirements/test.txt \
+  -r requirements/test-ci-base.txt \
+  -r requirements/test-integration.txt
+
+COPY --chown=1000:1000 MANIFEST.in Makefile setup.py setup.cfg tox.ini $HOME/
+COPY --chown=1000:1000 t $HOME/t
+COPY --chown=1000:1000 celery $HOME/celery
+
+RUN pyenv exec pip install -e .
+
+# the compiled files from earlier steps will cause py.test to fail with
+# an ImportMismatchError
+RUN make clean-pyc
+
+# Setup the entrypoint, this ensures pyenv is initialized when a container is started.
+ENTRYPOINT ["/entrypoint"]

+ 36 - 0
docker/docker-compose.yml

@@ -0,0 +1,36 @@
+version: '2'
+
+services:
+  celery:
+    build:
+      context: ..
+      dockerfile: docker/Dockerfile
+      args:
+        CELERY_USER:
+    environment:
+      TEST_BROKER: pyamqp://rabbit:5672
+      TEST_BACKEND: redis://redis
+      PYTHONUNBUFFERED: 1
+      PYTHONDONTWRITEBYTECODE: 1
+      REDIS_HOST: redis
+      WORKER_LOGLEVEL: DEBUG
+    tty: true
+    volumes:
+      - ../celery:/home/$CELERY_USER/celery
+      # Because pytest fails when it encounters files from alternative python compilations,
+      # __pycache__ and pyc files, PYTHONDONTWRITEBYTECODE must be
+      # set on the host as well or py.test will throw configuration errors.
+#      - ../t:/home/$CELERY_USER/t
+    depends_on:
+      - rabbit
+      - redis
+      - dynamodb
+
+  rabbit:
+    image: rabbitmq:3.7.3
+
+  redis:
+    image: redis:3.2.11
+
+  dynamodb:
+    image: dwmkerr/dynamodb:38

+ 4 - 0
docker/entrypoint

@@ -0,0 +1,4 @@
+#!/bin/bash
+eval "$(pyenv init -)"
+eval "$(pyenv virtualenv-init -)"
+exec "$@"

+ 3 - 0
docker/scripts/create-linux-user.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+addgroup --gid 1000 $CELERY_USER
+adduser --system --disabled-password --uid 1000 --gid 1000 $CELERY_USER

+ 5 - 0
docker/scripts/install-couchbase.sh

@@ -0,0 +1,5 @@
+#!/bin/sh
+wget http://packages.couchbase.com/clients/c/libcouchbase-2.8.4_jessie_amd64.tar
+tar -vxf libcouchbase-2.8.4_jessie_amd64.tar
+dpkg -i libcouchbase-2.8.4_jessie_amd64/libcouchbase2-core_2.8.4-1_amd64.deb
+dpkg -i libcouchbase-2.8.4_jessie_amd64/libcouchbase-dev_2.8.4-1_amd64.deb

+ 13 - 0
docker/scripts/install-pyenv.sh

@@ -0,0 +1,13 @@
+#!/bin/sh
+# For managing all the local python installations for testing, use pyenv
+curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
+
+# To enable testing versions like 3.4.8 as 3.4 in tox, we need to alias
+# pyenv python versions
+git clone https://github.com/s1341/pyenv-alias.git $(pyenv root)/plugins/pyenv-alias
+
+# Python versions to test against
+VERSION_ALIAS="python2.7" pyenv install 2.7.14
+VERSION_ALIAS="python3.4" pyenv install 3.4.8
+VERSION_ALIAS="python3.5" pyenv install 3.5.5
+VERSION_ALIAS="python3.6" pyenv install 3.6.4

+ 5 - 0
t/integration/conftest.py

@@ -24,6 +24,11 @@ def flaky(fun):
     return _inner
     return _inner
 
 
 
 
+def get_redis_connection():
+    from redis import StrictRedis
+    return StrictRedis(host=os.environ.get('REDIS_HOST'))
+
+
 @pytest.fixture(scope='session')
 @pytest.fixture(scope='session')
 def celery_config():
 def celery_config():
     return {
     return {

+ 5 - 7
t/integration/tasks.py

@@ -7,6 +7,8 @@ from celery import chain, group, shared_task
 from celery.exceptions import SoftTimeLimitExceeded
 from celery.exceptions import SoftTimeLimitExceeded
 from celery.utils.log import get_task_logger
 from celery.utils.log import get_task_logger
 
 
+from .conftest import get_redis_connection
+
 logger = get_task_logger(__name__)
 logger = get_task_logger(__name__)
 
 
 
 
@@ -115,17 +117,15 @@ def retry_once(self):
 @shared_task
 @shared_task
 def redis_echo(message):
 def redis_echo(message):
     """Task that appends the message to a redis list"""
     """Task that appends the message to a redis list"""
-    from redis import StrictRedis
 
 
-    redis_connection = StrictRedis()
+    redis_connection = get_redis_connection()
     redis_connection.rpush('redis-echo', message)
     redis_connection.rpush('redis-echo', message)
 
 
 
 
 @shared_task(bind=True)
 @shared_task(bind=True)
 def second_order_replace1(self, state=False):
 def second_order_replace1(self, state=False):
-    from redis import StrictRedis
 
 
-    redis_connection = StrictRedis()
+    redis_connection = get_redis_connection()
     if not state:
     if not state:
         redis_connection.rpush('redis-echo', 'In A')
         redis_connection.rpush('redis-echo', 'In A')
         new_task = chain(second_order_replace2.s(),
         new_task = chain(second_order_replace2.s(),
@@ -137,9 +137,7 @@ def second_order_replace1(self, state=False):
 
 
 @shared_task(bind=True)
 @shared_task(bind=True)
 def second_order_replace2(self, state=False):
 def second_order_replace2(self, state=False):
-    from redis import StrictRedis
-
-    redis_connection = StrictRedis()
+    redis_connection = get_redis_connection()
     if not state:
     if not state:
         redis_connection.rpush('redis-echo', 'In B')
         redis_connection.rpush('redis-echo', 'In B')
         new_task = chain(redis_echo.s("In/Out C"),
         new_task = chain(redis_echo.s("In/Out C"),

+ 4 - 5
t/integration/test_canvas.py

@@ -4,13 +4,12 @@ from datetime import datetime, timedelta
 from time import sleep
 from time import sleep
 
 
 import pytest
 import pytest
-from redis import StrictRedis
 
 
 from celery import chain, chord, group
 from celery import chain, chord, group
 from celery.exceptions import TimeoutError
 from celery.exceptions import TimeoutError
 from celery.result import AsyncResult, GroupResult
 from celery.result import AsyncResult, GroupResult
 
 
-from .conftest import flaky
+from .conftest import flaky, get_redis_connection
 from .tasks import (add, add_chord_to_chord, add_replaced, add_to_all,
 from .tasks import (add, add_chord_to_chord, add_replaced, add_to_all,
                     add_to_all_to_chord, collect_ids, delayed_sum,
                     add_to_all_to_chord, collect_ids, delayed_sum,
                     delayed_sum_with_soft_guard, identity, ids, print_unicode,
                     delayed_sum_with_soft_guard, identity, ids, print_unicode,
@@ -66,7 +65,7 @@ class test_chain:
 
 
         if not manager.app.conf.result_backend.startswith('redis'):
         if not manager.app.conf.result_backend.startswith('redis'):
             raise pytest.skip('Requires redis result backend.')
             raise pytest.skip('Requires redis result backend.')
-        redis_connection = StrictRedis()
+        redis_connection = get_redis_connection()
         redis_connection.delete('redis-echo')
         redis_connection.delete('redis-echo')
         before = group(redis_echo.si('before {}'.format(i)) for i in range(3))
         before = group(redis_echo.si('before {}'.format(i)) for i in range(3))
         connect = redis_echo.si('connect')
         connect = redis_echo.si('connect')
@@ -94,7 +93,7 @@ class test_chain:
         if not manager.app.conf.result_backend.startswith('redis'):
         if not manager.app.conf.result_backend.startswith('redis'):
             raise pytest.skip('Requires redis result backend.')
             raise pytest.skip('Requires redis result backend.')
 
 
-        redis_connection = StrictRedis()
+        redis_connection = get_redis_connection()
         redis_connection.delete('redis-echo')
         redis_connection.delete('redis-echo')
 
 
         result = second_order_replace1.delay()
         result = second_order_replace1.delay()
@@ -242,7 +241,7 @@ class test_chord:
         if not manager.app.conf.result_backend.startswith('redis'):
         if not manager.app.conf.result_backend.startswith('redis'):
             raise pytest.skip('Requires redis result backend.')
             raise pytest.skip('Requires redis result backend.')
 
 
-        redis_client = StrictRedis()
+        redis_client = get_redis_connection()
         async_result = chord([add.s(5, 6), add.s(6, 7)])(delayed_sum.s())
         async_result = chord([add.s(5, 6), add.s(6, 7)])(delayed_sum.s())
         for _ in range(TIMEOUT):
         for _ in range(TIMEOUT):
             if async_result.state == 'STARTED':
             if async_result.state == 'STARTED':

+ 1 - 0
tox.ini

@@ -34,6 +34,7 @@ commands =
     integration: py.test -xsv t/integration
     integration: py.test -xsv t/integration
 setenv =
 setenv =
     WORKER_LOGLEVEL = INFO
     WORKER_LOGLEVEL = INFO
+    PYTHONIOENCODING = UTF-8
 
 
     rabbitmq: TEST_BROKER=pyamqp://
     rabbitmq: TEST_BROKER=pyamqp://
     rabbitmq: TEST_BACKEND=rpc
     rabbitmq: TEST_BACKEND=rpc