diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a2780d5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,74 @@ +# This image provides a Python 3.5 environment you can use to run your Python +# applications. +FROM registry.fedoraproject.org/f25/s2i-base:latest + +LABEL MAINTAINER SoftwareCollections.org + +ENV NAME=python \ + VERSION=0 \ + RELEASE=1 \ + ARCH=x86_64 + +LABEL BZComponent="$NAME" \ + Name="$FGC/$NAME" \ + Version="$VERSION" \ + Release="$RELEASE.$DISTTAG" \ + Architecture="$ARCH" \ + Usage="s2i build file:///your/app 25/python your-app" \ + Help="help.1" + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.5 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + DESCRIPTION="Python $PYTHON_VERSION available as docker container, is a base platform for \ +building and running various Python $PYTHON_VERSION applications and frameworks. \ +Python is an easy to learn, powerful programming language. It has efficient high-level \ +data structures and a simple but effective approach to object-oriented programming. \ +Python’s elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$SUMMARY" \ + io.k8s.display-name="Python 3.5" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python35,rh-python35" + +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip python3-virtualenv \ + nss_wrapper httpd httpd-devel atlas-devel gcc-gfortran libffi-devel libtool-ltdl" && \ + dnf install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + dnf clean all -y + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY ./s2i/bin/ $STI_SCRIPTS_PATH + +# Each language image can have 'contrib' a directory with extra files needed to +# run and build the applications. +COPY ./contrib/ /opt/app-root + +# Add help file +COPY root / + +# Create a Python virtual environment for use by any application to avoid +# potential conflicts with Python packages preinstalled in the main Python +# installation. +RUN virtualenv-$VERSION /opt/app-root + +# In order to drop the root user, we have to make some directories world +# writable as OpenShift default security model is to run the container under +# random UID. +RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b069f7 --- /dev/null +++ b/README.md @@ -0,0 +1,246 @@ +Python Docker image +=================== + +This repository contains the source for building various versions of +the Python application as a reproducible Docker image using +[source-to-image](https://github.com/openshift/source-to-image). +Users can choose between RHEL and CentOS based builder images. +The resulting image can be run using [Docker](http://docker.io). + + +Usage +--------------------- +To build a simple [python-sample-app](https://github.com/openshift/s2i-python/tree/master/3.5/test/setup-test-app) application +using standalone [S2I](https://github.com/openshift/source-to-image) and then run the +resulting image with [Docker](http://docker.io) execute: + +* **For RHEL based image** + ``` + $ s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.5/test/setup-test-app/ rhscl/python-35-rhel7 python-sample-app + $ docker run -p 8080:8080 python-sample-app + ``` + +* **For CentOS based image** + ``` + $ s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.5/test/setup-test-app/ centos/python-35-centos7 python-sample-app + $ docker run -p 8080:8080 python-sample-app + ``` + +**Accessing the application:** +``` +$ curl 127.0.0.1:8080 +``` + + +Repository organization +------------------------ +* **``** + + * **Dockerfile** + + CentOS based Dockerfile. + + * **Dockerfile.rhel7** + + RHEL based Dockerfile. In order to perform build or test actions on this + Dockerfile you need to run the action on a properly subscribed RHEL machine. + + * **`s2i/bin/`** + + This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): + + * **assemble** + + Used to install the sources into the location where the application + will be run and prepare the application for deployment (eg. installing + dependencies, etc.) + + * **run** + + This script is responsible for running the application by using the + application web server. + + * **usage*** + + This script prints the usage of this image. + + * **`contrib/`** + + This folder contains a file with commonly used modules. + + * **`test/`** + + This folder contains a [S2I](https://github.com/openshift/source-to-image) + test framework with a simple server. + + * **`setup-test-app/`** + + Simple Gunicorn application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. + + * **`standalone-test-app/`** + + Simple standalone application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. + + * **run** + + Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. + + +Environment variables +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh`) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py`) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/openshift/s2i-python/tree/master/3.5/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_COLLECTSTATIC** + + Set this variable to a non-empty value to inhibit the execution of + 'manage.py collectstatic' during the build. This only affects Django projects. + +* **DISABLE_MIGRATE** + + Set this variable to a non-empty value to inhibit the execution of 'manage.py migrate' + when the produced image is run. This only affects Django projects. + +* **PIP_INDEX_URL** + + Set this variable to use a custom index URL or mirror to download required packages + during build process. This only affects packages listed in requirements.txt. + +* **UPGRADE_PIP_TO_LATEST** + + Set this variable to a non-empty value to have the 'pip' program be upgraded + to the most recent version before any Python packages are installed. If not + set it will use whatever the default version is included by the platform + for the Python version being used. + +* **WEB_CONCURRENCY** + + Set this to change the default setting for the number of + [workers](http://docs.gunicorn.org/en/stable/settings.html#workers). By + default, this is set to the number of available cores times 2. + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **setup.py** + + Configures various aspects of the project, including installation of + dependencies, as documented + [here](https://packaging.python.org/en/latest/distributing.html#setup-py). + For most projects, it is sufficient to simply use `requirements.txt`. + + +Run strategies +-------------- + +The Docker image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + +* **Django development server** + + If you have Django in your requirements but don't have Gunicorn, then your + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +--------------------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use Docker's +[exec](https://docs.docker.com/reference/commandline/exec/) command: + +``` +docker exec -it /bin/bash +``` + +After you enter into the running container, your current directory is set +to `/opt/app-root/src`, where the source code is located. diff --git a/contrib/etc/generate_container_user b/contrib/etc/generate_container_user new file mode 100644 index 0000000..43ee7ec --- /dev/null +++ b/contrib/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/sbin/nologin" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/root/help.1 b/root/help.1 new file mode 100644 index 0000000..8dd0973 --- /dev/null +++ b/root/help.1 @@ -0,0 +1,314 @@ +.TH "PYTHON-35-RHEL7" "1" " Container Image Pages" "Red Hat" "April 07, 2017" "" + + +.SH Python Docker image +.PP +This repository contains the source for building various versions of +the Python application as a reproducible Docker image using + +\[la]https://github.com/openshift/source-to-image\[ra]. +Users can choose between RHEL and CentOS based builder images. +The resulting image can be run using +\[la]http://docker.io\[ra]. + +.SH Usage +.PP +To build a simple +\[la]https://github.com/openshift/s2i-python/tree/master/3.5/test/setup-test-app\[ra] application +using standalone +\[la]https://github.com/openshift/source-to-image\[ra] and then run the +resulting image with +\[la]http://docker.io\[ra] execute: +.IP \n+[step] + +\item +.PP +\fBFor RHEL based image\fP +\fB\fC +$ s2i build https://github.com/sclorg/s2i\-python\-container.git \-\-context\-dir=3.5/test/setup\-test\-app/ rhscl/python\-35\-rhel7 python\-sample\-app +$ docker run \-p 8080:8080 python\-sample\-app +\fR +\item +.PP +\fBFor CentOS based image\fP +\fB\fC +$ s2i build https://github.com/sclorg/s2i\-python\-container.git \-\-context\-dir=3.5/test/setup\-test\-app/ centos/python\-35\-centos7 python\-sample\-app +$ docker run \-p 8080:8080 python\-sample\-app +\fR +.PP +\fBAccessing the application:\fP + +.PP +.RS + +.nf +$ curl 127.0.0.1:8080 + +.fi +.RE + +.SH Repository organization.IP \n+[step] + +\item +.PP +\fB\fB\fC\fR\fP.IP \n+[step] + +\item +.PP +\fBDockerfile\fP +.PP +CentOS based Dockerfile. +\item +.PP +\fBDockerfile.rhel7\fP +.PP +RHEL based Dockerfile. In order to perform build or test actions on this +Dockerfile you need to run the action on a properly subscribed RHEL machine. +\item +.PP +\fB\fB\fCs2i/bin/\fR\fP +.PP +This folder contains scripts that are run by +\[la]https://github.com/openshift/source-to-image\[ra]: +.IP \n+[step] + +\item +.PP +\fBassemble\fP +.PP +Used to install the sources into the location where the application +will be run and prepare the application for deployment (eg. installing +dependencies, etc.) +\item +.PP +\fBrun\fP +.PP +This script is responsible for running the application by using the +application web server. +\item +.PP +\fBusage\fP* +.PP +This script prints the usage of this image. +\item +.PP +\fB\fB\fCcontrib/\fR\fP +.PP +This folder contains a file with commonly used modules. +\item +.PP +\fB\fB\fCtest/\fR\fP +.PP +This folder contains a +\[la]https://github.com/openshift/source-to-image\[ra] +test framework with a simple server. +.IP \n+[step] + +\item +.PP +\fB\fB\fCsetup\-test\-app/\fR\fP +.PP +Simple Gunicorn application used for testing purposes by the +\[la]https://github.com/openshift/source-to-image\[ra] test framework. +\item +.PP +\fB\fB\fCstandalone\-test\-app/\fR\fP +.PP +Simple standalone application used for testing purposes by the +\[la]https://github.com/openshift/source-to-image\[ra] test framework. +\item +.PP +\fBrun\fP +.PP +Script that runs the +\[la]https://github.com/openshift/source-to-image\[ra] test framework. +.SH Environment variables +.PP +To set these environment variables, you can place them as a key value pair into a \fB\fC.s2i/environment\fR +file inside your source code repository. +.IP \n+[step] + +\item +.PP +\fBAPP\_SCRIPT\fP +.PP +Used to run the application from a script file. +This should be a path to a script file (defaults to \fB\fCapp.sh\fR) that will be +run to start the application. +\item +.PP +\fBAPP\_FILE\fP +.PP +Used to run the application from a Python script. +This should be a path to a Python file (defaults to \fB\fCapp.py\fR) that will be +passed to the Python interpreter to start the application. +\item +.PP +\fBAPP\_MODULE\fP +.PP +Used to run the application with Gunicorn, as documented + +\[la]http://docs.gunicorn.org/en/latest/run.html#gunicorn\[ra]. +This variable specifies a WSGI callable with the pattern +\fB\fCMODULE\_NAME:VARIABLE\_NAME\fR, where \fB\fCMODULE\_NAME\fR is the full dotted path +of a module, and \fB\fCVARIABLE\_NAME\fR refers to a WSGI callable inside the +specified module. +Gunicorn will look for a WSGI callable named \fB\fCapplication\fR if not specified. + +.PP +If \fB\fCAPP\_MODULE\fR is not provided, the \fB\fCrun\fR script will look for a \fB\fCwsgi.py\fR +file in your project and use it if it exists. + +.PP +If using \fB\fCsetup.py\fR for installing the application, the \fB\fCMODULE\_NAME\fR part +can be read from there. For an example, see + +\[la]https://github.com/openshift/s2i-python/tree/master/3.5/test/setup-test-app\[ra]. +\item +.PP +\fBAPP\_HOME\fP +.PP +This variable can be used to specify a sub\-directory in which the application to be run is contained. +The directory pointed to by this variable needs to contain \fB\fCwsgi.py\fR (for Gunicorn) or \fB\fCmanage.py\fR (for Django). + +.PP +If \fB\fCAPP\_HOME\fR is not provided, the \fB\fCassemble\fR and \fB\fCrun\fR scripts will use the application's root +directory. +\item +.PP +\fBAPP\_CONFIG\fP +.PP +Path to a valid Python file with a + +\[la]http://docs.gunicorn.org/en/latest/configure.html#configuration-file\[ra] file. +\item +.PP +\fBDISABLE\_COLLECTSTATIC\fP +.PP +Set this variable to a non\-empty value to inhibit the execution of +'manage.py collectstatic' during the build. This only affects Django projects. +\item +.PP +\fBDISABLE\_MIGRATE\fP +.PP +Set this variable to a non\-empty value to inhibit the execution of 'manage.py migrate' +when the produced image is run. This only affects Django projects. +\item +.PP +\fBPIP\_INDEX\_URL\fP +.PP +Set this variable to use a custom index URL or mirror to download required packages +during build process. This only affects packages listed in requirements.txt. +\item +.PP +\fBUPGRADE\_PIP\_TO\_LATEST\fP +.PP +Set this variable to a non\-empty value to have the 'pip' program be upgraded +to the most recent version before any Python packages are installed. If not +set it will use whatever the default version is included by the platform +for the Python version being used. +\item +.PP +\fBWEB\_CONCURRENCY\fP +.PP +Set this to change the default setting for the number of + +\[la]http://docs.gunicorn.org/en/stable/settings.html#workers\[ra]. By +default, this is set to the number of available cores times 2. +.SH Source repository layout +.PP +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: +.IP \n+[step] + +\item \fBrequirements.txt\fP +.PP +List of dependencies to be installed with \fB\fCpip\fR. The format is documented + +\[la]https://pip.pypa.io/en/latest/user_guide.html#requirements-files\[ra]. +.IP \n+[step] + +\item \fBsetup.py\fP +.PP +Configures various aspects of the project, including installation of + dependencies, as documented + +\[la]https://packaging.python.org/en/latest/distributing.html#setup-py\[ra]. + For most projects, it is sufficient to simply use \fB\fCrequirements.txt\fR. + +.SH Run strategies +.PP +The Docker image produced by s2i\-python executes your project in one of the +following ways, in precedence order: +.IP \n+[step] + +\item \fBGunicorn\fP +.PP +The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the \fB\fCrequirements.txt\fR + file or in the \fB\fCinstall\_requires\fR section of the \fB\fCsetup.py\fR file. + +.PP +If a file named \fB\fCwsgi.py\fR is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable \fB\fCAPP\_MODULE\fR. + This file is present in Django projects by default. + +.PP +If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. +.IP \n+[step] + +\item \fBDjango development server\fP +.PP +If you have Django in your requirements but don't have Gunicorn, then your + application will be served using Django's development web server. However, this is not + recommended for production environments. +.IP \n+[step] + +\item \fBPython script\fP +.PP +This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the \fB\fCAPP\_FILE\fR environment variable, defaulting to a + file named \fB\fCapp.py\fR if it exists. The script is passed to a regular + Python interpreter to launch your application. +.IP \n+[step] + +\item \fBApplication script file\fP +.PP +This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the \fB\fCAPP\_SCRIPT\fR environment variable, defaulting to a file named + \fB\fCapp.sh\fR if it exists. The script is executed directly to launch your + application. + +.SH Hot deploy +.PP +If you are using Django, hot deploy will work out of the box. + +.PP +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the + +\[la]https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload\[ra] +option set to \fB\fCtrue\fR. Make sure to specify your config via the \fB\fCAPP\_CONFIG\fR +environment variable. + +.PP +To change your source code in running container, use Docker's + +\[la]https://docs.docker.com/reference/commandline/exec/\[ra] command: + +.PP +.RS + +.nf +docker exec \-it /bin/bash + +.fi +.RE + +.PP +After you enter into the running container, your current directory is set +to \fB\fC/opt/app\-root/src\fR, where the source code is located. diff --git a/s2i/bin/assemble b/s2i/bin/assemble new file mode 100755 index 0000000..2d4c891 --- /dev/null +++ b/s2i/bin/assemble @@ -0,0 +1,64 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_collectstatic() { + is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] +} + +set -e + +shopt -s dotglob +echo "---> Installing application source ..." +mv /tmp/src/* ./ + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + echo "---> Upgrading pip to latest version ..." + pip install -U pip +fi + +if [[ -f requirements.txt ]]; then + echo "---> Installing dependencies ..." + pip install -r requirements.txt +fi + +if [[ -f setup.py ]]; then + echo "---> Installing application ..." + python setup.py develop +fi + +if should_collectstatic; then + ( + echo "---> Collecting Django static files ..." + + + APP_HOME=${APP_HOME:-.} + # Look for 'manage.py' in the directory specified by APP_HOME, or the current directory + manage_file=$APP_HOME/manage.py + + if [[ ! -f "$manage_file" ]]; then + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# Fix source directory permissions +fix-permissions ./ +# set permissions for any installed artifacts +fix-permissions /opt/app-root + +# remove pip temporary directory +rm -rf /tmp/pip_build_default diff --git a/s2i/bin/run b/s2i/bin/run new file mode 100755 index 0000000..ba3f68c --- /dev/null +++ b/s2i/bin/run @@ -0,0 +1,103 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 10 MiB for every worker + local default=$(((${MEMORY_LIMIT_IN_BYTES:-MAX_MEMORY_LIMIT_IN_BYTES}/1024/1024 - 43) / 10)) + default=$((default > max ? max : default)) + default=$((default < 1 ? 1 : default)) + echo $default +} + +app_script_check="${APP_SCRIPT-}" +APP_SCRIPT="${APP_SCRIPT:-app.sh}" +if [[ -f "$APP_SCRIPT" ]]; then + echo "---> Running application from script ($APP_SCRIPT) ..." + if [[ "$APP_SCRIPT" != /* ]]; then + APP_SCRIPT="./$APP_SCRIPT" + fi + exec "$APP_SCRIPT" +else + test -n "$app_script_check" && (>&2 echo "ERROR: file '$app_script_check' not found.") && exit 1 +fi + +app_file_check="${APP_FILE-}" +APP_FILE="${APP_FILE:-app.py}" +if [[ -f "$APP_FILE" ]]; then + echo "---> Running application from Python script ($APP_FILE) ..." + exec python "$APP_FILE" +else + test -n "$app_file_check" && (>&2 echo "ERROR: file '$app_file_check' not found.") && exit 1 +fi + +APP_HOME=${APP_HOME:-.} +# Look for 'manage.py' in the directory specified by APP_HOME, or the current direcotry +manage_file=$APP_HOME/manage.py + +if should_migrate; then + if [[ -f "$manage_file" ]]; then + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +if is_gunicorn_installed; then + if [[ -z "$APP_MODULE" ]]; then + # Look only in the directory specified by APP_HOME, or the current directory + # replace all "/" with ".", remove leading "." and ".py" suffix + APP_MODULE=$(find $APP_HOME -maxdepth 1 -type f -name 'wsgi.py' | sed 's:/:.:g;s:^\.\+::;s:\.py$::') + fi + + if [[ -z "$APP_MODULE" && -f setup.py ]]; then + APP_MODULE="$(python setup.py --name)" + fi + + if [[ "$APP_MODULE" ]]; then + export WEB_CONCURRENCY=${WEB_CONCURRENCY:-$(get_default_web_concurrency)} + + echo "---> Serving application with gunicorn ($APP_MODULE) ..." + exec gunicorn "$APP_MODULE" --bind=0.0.0.0:8080 --access-logfile=- --config "$APP_CONFIG" + fi +fi + +if is_django_installed; then + if [[ -f "$manage_file" ]]; then + echo "---> Serving application with 'manage.py runserver' ..." + echo "WARNING: this is NOT a recommended way to run you application in production!" + echo "Consider using gunicorn or some other production web server." + exec python "$manage_file" runserver 0.0.0.0:8080 + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py runserver'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/s2i/bin/usage b/s2i/bin/usage new file mode 100755 index 0000000..09b28d8 --- /dev/null +++ b/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat </dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + s2i build ${s2i_args} file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + exit 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup_app() { + info "Cleaning up app container ..." + if [ -f $cid_file ]; then + if container_exists; then + docker stop $(cat $cid_file) + fi + fi +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} + +check_result() { + local result="$1" + if [[ "$result" != "0" ]]; then + info "TEST FAILED (${result})" + cleanup + exit $result + fi +} + +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + local result=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && break + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing the image SCL enable" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file=$(mktemp -u --suffix=.cid) + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + + test_scl_usage "python --version" "Python 3.5." "${cid_file}" + check_result $? + test_connection + check_result $? + cleanup_app +} + + +# Since we built the candidate image locally, we don't want S2I attempt to pull +# it from Docker hub +s2i_args="--force-pull=false" + +# Verify the 'usage' script is working properly when running the base image with 's2i usage ...' +test_s2i_usage +check_result $? + +# Verify the 'usage' script is working properly when running the base image with 'docker run ...' +test_docker_run_usage +check_result $? + +for app in ${WEB_APPS[@]}; do + prepare ${app} + run_s2i_build ${app} + check_result $? + + # test application with default user + test_application + + # test application with random user + CONTAINER_ARGS="-u 12345" test_application + + info "All tests for the ${app} finished successfully." + cleanup ${app} +done + +info "All tests finished successfully." diff --git a/test/setup-test-app/.gitignore b/test/setup-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/setup-test-app/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/test/setup-test-app/setup.py b/test/setup-test-app/setup.py new file mode 100644 index 0000000..e313409 --- /dev/null +++ b/test/setup-test-app/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + + +setup ( + name = "testapp", + version = "0.1", + description = "Example application to be deployed.", + packages = find_packages(), + install_requires = ["gunicorn"], +) diff --git a/test/setup-test-app/testapp.py b/test/setup-test-app/testapp.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/setup-test-app/testapp.py @@ -0,0 +1,4 @@ + +def application(environ, start_response): + start_response('200 OK', [('Content-Type','text/plain')]) + return [b"Hello from gunicorn WSGI application!"] diff --git a/test/standalone-test-app/app.py b/test/standalone-test-app/app.py new file mode 100644 index 0000000..d786ee7 --- /dev/null +++ b/test/standalone-test-app/app.py @@ -0,0 +1,27 @@ +import os + +from gunicorn.app.base import BaseApplication +from gunicorn.six import iteritems + + +def wsgi_handler(environ, start_response): + start_response('200 OK', [('Content-Type','text/html')]) + return [b"Hello World from standalone WSGI application!"] + +class StandaloneApplication(BaseApplication): + def __init__(self, app, options=None): + self.options = options or {} + self.application = app + super(StandaloneApplication, self).__init__() + + def load_config(self): + config = dict([(key, value) for key, value in iteritems(self.options) + if key in self.cfg.settings and value is not None]) + for key, value in iteritems(config): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + +if __name__ == '__main__': + StandaloneApplication(wsgi_handler, {'bind': ':8080'}).run() diff --git a/test/standalone-test-app/requirements.txt b/test/standalone-test-app/requirements.txt new file mode 100644 index 0000000..8f22dcc --- /dev/null +++ b/test/standalone-test-app/requirements.txt @@ -0,0 +1 @@ +gunicorn diff --git a/test/uwsgi-test-app/app.sh b/test/uwsgi-test-app/app.sh new file mode 100755 index 0000000..f6c873e --- /dev/null +++ b/test/uwsgi-test-app/app.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +exec uwsgi \ + --http-socket :8080 \ + --die-on-term \ + --master \ + --single-interpreter \ + --enable-threads \ + --threads=5 \ + --thunder-lock \ + --module wsgi diff --git a/test/uwsgi-test-app/requirements.txt b/test/uwsgi-test-app/requirements.txt new file mode 100644 index 0000000..66d4205 --- /dev/null +++ b/test/uwsgi-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/test/uwsgi-test-app/wsgi.py b/test/uwsgi-test-app/wsgi.py new file mode 100644 index 0000000..96e1a61 --- /dev/null +++ b/test/uwsgi-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run()