Compare commits

...
Sign in to create a new pull request.

3 commits

Author SHA1 Message Date
Marek Skalický
dd07e4dffc
Add versioned symlink - needed by some images. 2018-10-29 14:11:52 +00:00
Marek Skalický
8de2f6ec06
Pull changes from upstream repository. 2018-10-29 09:36:11 +00:00
Clement Verna
ff46c0a1f0 Drop Release label in favor of OSBS release_bump plugin.
OSBS can automatically bump the release number, for that
we just need to drop the label from the Dockerfile

See https://pagure.io/ContainerSIG/container-sig/issue/1

Signed-off-by: Clement Verna <cverna@tutanota.com>
2018-08-27 10:14:00 +02:00
29 changed files with 2264 additions and 132 deletions

1
3.6 Symbolic link
View file

@ -0,0 +1 @@
.

View file

@ -1,6 +1,6 @@
# This image provides a Python 3.5 environment you can use to run your Python
# This image provides a Python 3.6 environment you can use to run your Python
# applications.
FROM registry.fedoraproject.org/f26/s2i-base:latest
FROM registry.fedoraproject.org/f28/s2i-base:latest
EXPOSE 8080
@ -14,11 +14,10 @@ ENV PYTHON_VERSION=3.6 \
ENV NAME=python3 \
VERSION=0 \
RELEASE=1 \
ARCH=x86_64
ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \
DESCRIPTION="Python $PYTHON_VERSION available as docker container is a base platform for \
DESCRIPTION="Python $PYTHON_VERSION available as 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. \
@ -35,16 +34,15 @@ LABEL summary="$SUMMARY" \
com.redhat.component="$NAME" \
name="$FGC/$NAME" \
version="$VERSION" \
release="$RELEASE.$DISTTAG" \
architecture="$ARCH" \
usage="s2i build file:///your/app 26/python3 your-app" \
usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.6/test/setup-test-app/ $FGC/$NAME python-sample-app" \
maintainer="SoftwareCollections.org <sclorg@redhat.com>"
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 && \
nss_wrapper httpd httpd-devel atlas-devel gcc-gfortran \
libffi-devel libtool-ltdl enchant" && \
dnf -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \
rpm -V $INSTALL_PKGS && \
dnf clean all -y
dnf -y clean all --enablerepo='*'
# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH.
COPY ./s2i/bin/ $STI_SCRIPTS_PATH
@ -52,15 +50,21 @@ COPY ./s2i/bin/ $STI_SCRIPTS_PATH
# Copy extra files to the image.
COPY ./root/ /
# Create a Python virtual environment for use by any application to avoid
# - 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-$PYTHON_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 virtualenv-$PYTHON_VERSION ${APP_ROOT} && \
chown -R 1001:0 ${APP_ROOT} && \
fix-permissions ${APP_ROOT} -P
# 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
# For Fedora scl_enable isn't sourced automatically in s2i-core
# so virtualenv needs to be activated this way
ENV BASH_ENV="${APP_ROOT}/bin/activate" \
ENV="${APP_ROOT}/bin/activate" \
PROMPT_COMMAND=". ${APP_ROOT}/bin/activate"
USER 1001

1
Dockerfile.fedora Symbolic link
View file

@ -0,0 +1 @@
Dockerfile

140
README.md
View file

@ -1,16 +1,33 @@
Python Docker image
Python 3.6 container 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).
This container image includes Python 3.6 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.6 applications.
Users can choose between RHEL and CentOS based builder images.
The RHEL image is available in the [Red Hat Container Catalog](https://access.redhat.com/containers/#/registry.access.redhat.com/rhscl/python-36-rhel7)
as registry.access.redhat.com/rhscl/python-36-rhel7.
The CentOS image is then available on [Docker Hub](https://hub.docker.com/r/centos/python-36-centos7/)
as centos/python-36-centos7.
The resulting image can be run using [Docker](http://docker.io).
Description
-----------
Python 3.6 available as container is a base platform for
building and running various Python 3.6 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.
This container image includes an npm utility, so users can use it to install JavaScript
modules for their web applications. There is no guarantee for any specific npm or nodejs
version, that is included in the image; those versions can be changed anytime and
the nodejs itself is included just to make the npm work.
Usage
---------------------
To build a simple [python-sample-app](https://github.com/openshift/s2i-python/tree/master/3.6/test/setup-test-app) application
To build a simple [python-sample-app](https://github.com/sclorg/s2i-python-container/tree/master/3.6/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:
@ -31,61 +48,6 @@ resulting image with [Docker](http://docker.io) execute:
$ curl 127.0.0.1:8080
```
Repository organization
------------------------
* **`<python-version>`**
* **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
---------------------
@ -119,7 +81,7 @@ file inside your source code repository.
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.6/test/setup-test-app).
[setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.6/test/setup-test-app).
* **APP_HOME**
@ -134,33 +96,52 @@ file inside your source code repository.
Path to a valid Python file with a
[Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file.
* **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. See
"Handling Database Migrations" section of [Django blogpost on OpenShift blog](
https://blog.openshift.com/migrating-django-applications-openshift-3/) on suggestions
how/when to run DB migrations in OpenShift environment. Most importantly,
note that running DB migrations from two or more pods might corrupt your database.
* **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**
* **DISABLE_SETUP_PY_PROCESSING**
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.
Set this to a non-empty value to skip processing of setup.py script if you
use `-e .` in requirements.txt to trigger its processing or you don't want
your application to be installed into site-packages directory.
* **ENABLE_PIPENV**
Set this variable to use [Pipenv](https://github.com/kennethreitz/pipenv),
the higher-level Python packaging tool, to manage dependencies of the application.
This should be used only if your project contains properly formated Pipfile
and Pipfile.lock.
* **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.
Pipenv ignores this variable.
* **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.
Set this variable to a non-empty value to have the 'pip' program and related
python packages (setuptools and wheel) 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.
default, this is set to the number of available cores times 2, capped
at 12.
Source repository layout
------------------------
@ -174,18 +155,26 @@ However, if these files exist they will affect the behavior of the build process
[here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files).
* **Pipfile**
The replacement for requirements.txt, project is currently under active
design and development, as documented [here](https://github.com/pypa/pipfile).
Set `ENABLE_PIPENV` environment variable to true in order to process this file.
* **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`.
For most projects, it is sufficient to simply use `requirements.txt` or
`Pipfile`. Set `DISABLE_SETUP_PY_PROCESSING` environment variable to true
in order to skip processing of this file.
Run strategies
--------------
The Docker image produced by s2i-python executes your project in one of the
The container image produced by s2i-python executes your project in one of the
following ways, in precedence order:
* **Gunicorn**
@ -244,3 +233,10 @@ docker exec -it <CONTAINER_ID> /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.
See also
--------
Dockerfile and other sources are available on https://github.com/sclorg/s2i-python-container.
In that repository you also can find another versions of Python environment Dockerfiles.
Dockerfile for CentOS is called Dockerfile, Dockerfile for RHEL is called Dockerfile.rhel7.

1
help.md Symbolic link
View file

@ -0,0 +1 @@
README.md

317
root/help.1 Normal file
View file

@ -0,0 +1,317 @@
.TH Python 3.6 container image
.PP
This container image includes Python 3.6 as a S2I
\[la]https://github.com/openshift/source-to-image\[ra] base image for your Python 3.6 applications.
Users can choose between RHEL and CentOS based builder images.
The RHEL image is available in the Red Hat Container Catalog
\[la]https://access.redhat.com/containers/#/registry.access.redhat.com/rhscl/python-36-rhel7\[ra]
as registry.access.redhat.com/rhscl/python\-36\-rhel7.
The CentOS image is then available on Docker Hub
\[la]https://hub.docker.com/r/centos/python-36-centos7/\[ra]
as centos/python\-36\-centos7.
The resulting image can be run using Docker
\[la]http://docker.io\[ra]\&.
.SH Description
.PP
Python 3.6 available as container is a base platform for
building and running various Python 3.6 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.
.PP
This container image includes an npm utility, so users can use it to install JavaScript
modules for their web applications. There is no guarantee for any specific npm or nodejs
version, that is included in the image; those versions can be changed anytime and
the nodejs itself is included just to make the npm work.
.SH Usage
.PP
To build a simple python\-sample\-app
\[la]https://github.com/sclorg/s2i-python-container/tree/master/3.6/test/setup-test-app\[ra] application
using standalone S2I
\[la]https://github.com/openshift/source-to-image\[ra] and then run the
resulting image with Docker
\[la]http://docker.io\[ra] execute:
.IP \(bu 2
.PP
\fBFor RHEL based image\fP
.PP
.RS
.nf
$ s2i build https://github.com/sclorg/s2i\-python\-container.git \-\-context\-dir=3.6/test/setup\-test\-app/ rhscl/python\-36\-rhel7 python\-sample\-app
$ docker run \-p 8080:8080 python\-sample\-app
.fi
.RE
.IP \(bu 2
.PP
\fBFor CentOS based image\fP
.PP
.RS
.nf
$ s2i build https://github.com/sclorg/s2i\-python\-container.git \-\-context\-dir=3.6/test/setup\-test\-app/ centos/python\-36\-centos7 python\-sample\-app
$ docker run \-p 8080:8080 python\-sample\-app
.fi
.RE
.PP
\fBAccessing the application:\fP
.PP
.RS
.nf
$ curl 127.0.0.1:8080
.fi
.RE
.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 \(bu 2
.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 unless set to null) that will be
run to start the application.
.IP \(bu 2
.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 unless set to null) that will be
passed to the Python interpreter to start the application.
.IP \(bu 2
.PP
\fBAPP\_MODULE\fP
.PP
Used to run the application with Gunicorn, as documented
here
\[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
setup\-test\-app
\[la]https://github.com/sclorg/s2i-python-container/tree/master/3.6/test/setup-test-app\[ra]\&.
.IP \(bu 2
.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.
.IP \(bu 2
.PP
\fBAPP\_CONFIG\fP
.PP
Path to a valid Python file with a
Gunicorn configuration
\[la]http://docs.gunicorn.org/en/latest/configure.html#configuration-file\[ra] file.
.IP \(bu 2
.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. See
"Handling Database Migrations" section of Django blogpost on OpenShift blog
\[la]https://blog.openshift.com/migrating-django-applications-openshift-3/\[ra] on suggestions
how/when to run DB migrations in OpenShift environment. Most importantly,
note that running DB migrations from two or more pods might corrupt your database.
.IP \(bu 2
.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.
.IP \(bu 2
.PP
\fBDISABLE\_SETUP\_PY\_PROCESSING\fP
.PP
Set this to a non\-empty value to skip processing of setup.py script if you
use \fB\fC\-e .\fR in requirements.txt to trigger its processing or you don't want
your application to be installed into site\-packages directory.
.IP \(bu 2
.PP
\fBENABLE\_PIPENV\fP
.PP
Set this variable to use Pipenv
\[la]https://github.com/kennethreitz/pipenv\[ra],
the higher\-level Python packaging tool, to manage dependencies of the application.
This should be used only if your project contains properly formated Pipfile
and Pipfile.lock.
.IP \(bu 2
.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.
Pipenv ignores this variable.
.IP \(bu 2
.PP
\fBUPGRADE\_PIP\_TO\_LATEST\fP
.PP
Set this variable to a non\-empty value to have the 'pip' program and related
python packages (setuptools and wheel) 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.
.IP \(bu 2
.PP
\fBWEB\_CONCURRENCY\fP
.PP
Set this to change the default setting for the number of
workers
\[la]http://docs.gunicorn.org/en/stable/settings.html#workers\[ra]\&. By
default, this is set to the number of available cores times 2, capped
at 12.
.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 \(bu 2
\fBrequirements.txt\fP
.PP
List of dependencies to be installed with \fB\fCpip\fR\&. The format is documented
here
\[la]https://pip.pypa.io/en/latest/user_guide.html#requirements-files\[ra]\&.
.IP \(bu 2
\fBPipfile\fP
.PP
The replacement for requirements.txt, project is currently under active
design and development, as documented here
\[la]https://github.com/pypa/pipfile\[ra]\&.
Set \fB\fCENABLE\_PIPENV\fR environment variable to true in order to process this file.
.IP \(bu 2
\fBsetup.py\fP
.PP
Configures various aspects of the project, including installation of
dependencies, as documented
here
\[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 or
\fB\fCPipfile\fR\&. Set \fB\fCDISABLE\_SETUP\_PY\_PROCESSING\fR environment variable to true
in order to skip processing of this file.
.SH Run strategies
.PP
The container image produced by s2i\-python executes your project in one of the
following ways, in precedence order:
.IP \(bu 2
\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 \(bu 2
\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 \(bu 2
\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 \(bu 2
\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
\fB\fCreload\fR
\[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
exec
\[la]https://docs.docker.com/reference/commandline/exec/\[ra] command:
.PP
.RS
.nf
docker exec \-it <CONTAINER\_ID> /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.
.SH See also
.PP
Dockerfile and other sources are available on
\[la]https://github.com/sclorg/s2i-python-container\[ra]\&.
In that repository you also can find another versions of Python environment Dockerfiles.
Dockerfile for CentOS is called Dockerfile, Dockerfile for RHEL is called Dockerfile.rhel7.

View file

@ -2,5 +2,5 @@
# doing. This file is sourced everytime the shell session is opened.
# This will make scl collection binaries work out of box.
unset BASH_ENV PROMPT_COMMAND ENV
source scl_source enable httpd24 rh-python36
source scl_source enable httpd24 $NODEJS_SCL rh-python36
source /opt/app-root/bin/activate

View file

@ -8,6 +8,28 @@ function should_collectstatic() {
is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]]
}
function virtualenv_bin() {
if head "/etc/redhat-release" | grep -q "^Fedora"; then
virtualenv-${PYTHON_VERSION} $1
else
virtualenv $1
fi
}
# Install pipenv to the separate virtualenv to isolate it
# from system Python packages and packages in the main
# virtualenv. Executable is simlinked into ~/.local/bin
# to be accessible. This approach is inspired by pipsi
# (pip script installer).
function install_pipenv() {
echo "---> Installing pipenv packaging tool ..."
VENV_DIR=$HOME/.local/venvs/pipenv
virtualenv_bin "$VENV_DIR"
$VENV_DIR/bin/pip --isolated install -U pipenv
mkdir -p $HOME/.local/bin
ln -s $VENV_DIR/bin/pipenv $HOME/.local/bin/pipenv
}
set -e
shopt -s dotglob
@ -16,17 +38,26 @@ mv /tmp/src/* ./
if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then
echo "---> Upgrading pip to latest version ..."
pip install -U pip
pip install -U pip setuptools wheel
fi
if [[ -f requirements.txt ]]; then
if [[ ! -z "$ENABLE_PIPENV" ]]; then
install_pipenv
echo "---> Installing dependencies via pipenv ..."
if [[ -f Pipfile ]]; then
pipenv install --deploy
elif [[ -f requirements.txt ]]; then
pipenv install -r requirements.txt
fi
# pipenv check
elif [[ -f requirements.txt ]]; then
echo "---> Installing dependencies ..."
pip install -r requirements.txt
fi
if [[ -f setup.py ]]; then
if [[ -f setup.py && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then
echo "---> Installing application ..."
python setup.py develop
pip install .
fi
if should_collectstatic; then

View file

@ -29,6 +29,9 @@ function get_default_web_concurrency() {
local default=$(((${MEMORY_LIMIT_IN_BYTES:-MAX_MEMORY_LIMIT_IN_BYTES}/1024/1024 - 43) / 40))
default=$((default > max ? max : default))
default=$((default < 1 ? 1 : default))
# According to http://docs.gunicorn.org/en/stable/design.html#how-many-workers,
# 12 workers should be enough to handle hundreds or thousands requests per second
default=$((default > 12 ? 12 : default))
echo $default
}

View file

View file

@ -0,0 +1,75 @@
#!/bin/bash
# Test virtualenv environment and pip upgrade
# Get the latest stable version of the package released on PyPI
function get_latest_stable_version() {
echo $(curl -sL 'https://pypi.org/pypi/'$1'/json' | python -c \
"""
import sys
import json
from pip._vendor.packaging.version import parse
versions = [parse(v) for v in json.load(sys.stdin)['releases'].keys()]
print(str((sorted([v for v in versions if not v.is_prerelease])[-1])))
""")
}
# Get version of the package installed on the system
function get_installed_version() {
echo $(pip freeze --all | grep $1 | cut -d"=" -f3)
}
echo "Testing that the virtual environment's Python is being used ..."
if [ "$(which python)" != "/opt/app-root/bin/python" ]; then
echo "ERROR: Initialization of the virtual environment failed."
exit 1
fi
echo "Testing UPGRADE_PIP_TO_LATEST=1 (set in .s2i/environment) ..."
packages=("pip" "setuptools" "wheel")
for pkg in ${packages[@]}; do
if [ $(get_latest_stable_version $pkg) != $(get_installed_version $pkg) ]; then
echo "ERROR: Failed to upgrade '$pkg' to the latest version."
exit 1
fi
done
# Test that npm works correctly
# Create a package.json file so npm doesn't throw off warnings
echo "Initializing npm and testing the installation and use of a module ..."
npm init -y
# mkdirp chosen because it's on the list of 'most depended-upon packages':
# https://www.npmjs.com/browse/depended
npm install mkdirp
if [ $? -ne 0 ]; then
echo "ERROR: Installation of npm module 'mkdirp' failed"
exit 1
fi
# Use the mkdirp module to create a directory and it's subdirectory
node -e "var mkdirp = require('mkdirp'); mkdirp('dir1/dir2')"
if [ $? -ne 0 ]; then
echo "ERROR: Nodejs cannot use the freshly installed 'mkdirp' module."
exit 1
fi
if [ "$(ls dir1)" != "dir2" ]; then
echo "ERROR: The 'mkdirp' module could not create subdirectories at the current path."
exit 1
fi
# Test the uwsgi server
exec uwsgi \
--http-socket :8080 \
--die-on-term \
--master \
--single-interpreter \
--enable-threads \
--threads=5 \
--thunder-lock \
--module wsgi

57
test/pipenv-test-app/.gitignore vendored Normal file
View file

@ -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/

View file

@ -0,0 +1,2 @@
ENABLE_PIPENV=true
DISABLE_SETUP_PY_PROCESSING=true

View file

@ -0,0 +1,14 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
"e1839a8" = {path = ".", editable = true}
requests = "==2.17.0"
[dev-packages]
pytest = ">=2.8.0"
[requires]
python_version = "3.6"

95
test/pipenv-test-app/Pipfile.lock generated Normal file
View file

@ -0,0 +1,95 @@
{
"_meta": {
"hash": {
"sha256": "96a91e22aeebfddebc44a2cbc2c5db51b4040ca72b3cbff2af387ad2603cd5c5"
},
"host-environment-markers": {
"implementation_name": "cpython",
"implementation_version": "3.6.2",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "4.13.5-200.fc26.x86_64",
"platform_system": "Linux",
"platform_version": "#1 SMP Thu Oct 5 16:53:13 UTC 2017",
"python_full_version": "3.6.2",
"python_version": "3.6",
"sys_platform": "linux"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:54a07c09c586b0e4c619f02a5e94e36619da8e2b053e20f594348c0611803704",
"sha256:40523d2efb60523e113b44602298f0960e900388cf3bb6043f645cf57ea9e3f5"
],
"version": "==2017.7.27.1"
},
"chardet": {
"hashes": [
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691",
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"
],
"version": "==3.0.4"
},
"e1839a8": {
"editable": true,
"path": "."
},
"gunicorn": {
"hashes": [
"sha256:75af03c99389535f218cc596c7de74df4763803f7b63eb09d77e92b3956b36c6",
"sha256:eee1169f0ca667be05db3351a0960765620dad53f53434262ff8901b68a1b622"
],
"version": "==19.7.1"
},
"idna": {
"hashes": [
"sha256:cc19709fd6d0cbfed39ea875d29ba6d4e22c0cebc510a76d6302a28385e8bb70",
"sha256:3cb5ce08046c4e3a560fc02f138d0ac63e00f8ce5901a56b32ec8b7994082aab"
],
"version": "==2.5"
},
"requests": {
"hashes": [
"sha256:73b4088c05f7fb5ca8e68651ed802df3ca40621281acf74bb321b4a8408aab7e",
"sha256:eff227db5864238d44270cbadc8ac4133e69b69a2e7092b7b316ed1e4761cbd6"
],
"version": "==2.17.0"
},
"urllib3": {
"hashes": [
"sha256:8ed6d5c1ff9d6ba84677310060d6a3a78ca3072ce0684cb3c645023009c114b1",
"sha256:b14486978518ca0901a76ba973d7821047409d7f726f22156b24e83fd71382a5"
],
"version": "==1.21.1"
}
},
"develop": {
"py": {
"hashes": [
"sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a",
"sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"
],
"version": "==1.4.34"
},
"pytest": {
"hashes": [
"sha256:81a25f36a97da3313e1125fce9e7bbbba565bc7fec3c5beb14c262ddab238ac1",
"sha256:27fa6617efc2869d3e969a3e75ec060375bfb28831ade8b5cdd68da3a741dc3c"
],
"version": "==3.2.3"
}
}
}

View file

@ -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"],
)

View file

@ -0,0 +1,7 @@
import requests
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
assert requests.__version__ == '2.17.0'
return [b"Hello from gunicorn WSGI application!"]

View file

@ -6,14 +6,21 @@
# IMAGE_NAME specifies a name of the candidate image used for testing.
# The image has to be available before this script is executed.
#
IMAGE_NAME=${IMAGE_NAME:-openshift/python-36-centos7-candidate}
declare -a WEB_APPS=({standalone,setup,django,numpy,app-home,virtualenv-uwsgi,locale,mod-wsgi}-test-app)
declare -a WEB_APPS=({standalone,setup,setup-requirements,django,numpy,app-home,npm-virtualenv-uwsgi,locale,mod-wsgi,pipenv}-test-app)
# TODO: Make command compatible for Mac users
test_dir="$(readlink -zf $(dirname "${BASH_SOURCE[0]}"))"
image_dir=$(readlink -zf ${test_dir}/..)
if [[ -z $VERSION ]]; then
echo "ERROR: The VERSION variable must be set."
exit 1
fi
IMAGE_NAME=${IMAGE_NAME:-centos/python-${VERSION//./}-centos7}
. test/test-lib.sh
# TODO: This should be part of the image metadata
test_port=8080
@ -35,7 +42,7 @@ container_ip() {
run_s2i_build() {
info "Building the ${1} application image ..."
s2i build ${s2i_args} file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp
ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args}
}
prepare() {
@ -98,7 +105,7 @@ wait_for_cid() {
test_s2i_usage() {
info "Testing 's2i usage' ..."
s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null
ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null
}
test_docker_run_usage() {
@ -158,7 +165,11 @@ test_application() {
# Wait for the container to write it's CID file
wait_for_cid
test_scl_usage "python --version" "Python 3.6." "${cid_file}"
test_scl_usage "python --version" "Python $VERSION." "${cid_file}"
check_result $?
test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}"
check_result $?
test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}"
check_result $?
test_connection
check_result $?
@ -168,7 +179,7 @@ test_application() {
# Since we built the candidate image locally, we don't want S2I attempt to pull
# it from Docker hub
s2i_args="--force-pull=false"
s2i_args="--pull-policy=never"
# Verify the 'usage' script is working properly when running the base image with 's2i usage ...'
test_s2i_usage
@ -187,7 +198,7 @@ for app in ${WEB_APPS[@]}; do
test_application
# test application with random user
CONTAINER_ARGS="-u 12345" test_application
CONTAINER_ARGS="--user 12345" test_application
info "All tests for the ${app} finished successfully."
cleanup ${app}

32
test/run-openshift Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
#
# Test the Python image in the OpenShift.
#
# IMAGE_NAME specifies a name of the candidate image used for testing.
# VERSION specifies a version of the python in the candidate image.
# The image has to be available before this script is executed.
THISDIR=$(dirname ${BASH_SOURCE[0]})
source "${THISDIR}/test-lib.sh"
source "${THISDIR}/test-lib-openshift.sh"
set -exo nounset
test -n "${IMAGE_NAME-}" || false 'make sure $IMAGE_NAME is defined'
test -n "${VERSION-}" || false 'make sure $VERSION is defined'
ct_os_cluster_up
ct_os_test_s2i_app "${IMAGE_NAME}" "${THISDIR}/standalone-test-app" . "Hello World from standalone WSGI application!"
ct_os_test_s2i_app "${IMAGE_NAME}" "https://github.com/sclorg/django-ex.git" . 'Welcome to your Django application on OpenShift'
# TODO: update the template reference to proper location, once https://github.com/sclorg/django-ex/pull/115 is merged
ct_os_test_template_app "${IMAGE_NAME}" \
https://raw.githubusercontent.com/hhorak/django-ex/python-3.6/openshift/templates/django-postgresql.json \
python \
'Welcome to your Django application on OpenShift' \
8080 http 200 "-p SOURCE_REPOSITORY_REF=master -p PYTHON_VERSION=${VERSION} -p POSTGRESQL_VERSION=9.6 -p NAME=python-testing" \
"centos/postgresql-96-centos7|postgresql:9.6"

View file

@ -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/

View file

@ -0,0 +1 @@
-e .

View file

@ -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"],
)

View file

@ -0,0 +1,4 @@
def application(environ, start_response):
start_response('200 OK', [('Content-Type','text/plain')])
return [b"Hello from gunicorn WSGI application!"]

925
test/test-lib-openshift.sh Normal file
View file

@ -0,0 +1,925 @@
# Set of functions for testing docker images in OpenShift using 'oc' command
# ct_os_get_status
# --------------------
# Returns status of all objects to make debugging easier.
function ct_os_get_status() {
oc get all
oc status
}
# ct_os_print_logs
# --------------------
# Returns status of all objects and logs from all pods.
function ct_os_print_logs() {
ct_os_get_status
while read pod_name; do
echo "INFO: printing logs for pod ${pod_name}"
oc logs ${pod_name}
done < <(oc get pods --no-headers=true -o custom-columns=NAME:.metadata.name)
}
# ct_os_enable_print_logs
# --------------------
# Enables automatic printing of pod logs on ERR.
function ct_os_enable_print_logs() {
set -E
trap ct_os_print_logs ERR
}
# ct_get_public_ip
# --------------------
# Returns best guess for the IP that the node is accessible from other computers.
# This is a bit funny heuristic, simply goes through all IPv4 addresses that
# hostname -I returns and de-prioritizes IP addresses commonly used for local
# addressing. The rest of addresses are taken as public with higher probability.
function ct_get_public_ip() {
local hostnames=$(hostname -I)
local public_ip=''
local found_ip
for guess_exp in '127\.0\.0\.1' '192\.168\.[0-9\.]*' '172\.[0-9\.]*' \
'10\.[0-9\.]*' '[0-9\.]*' ; do
found_ip=$(echo "${hostnames}" | grep -oe "${guess_exp}")
if [ -n "${found_ip}" ] ; then
hostnames=$(echo "${hostnames}" | sed -e "s/${found_ip}//")
public_ip="${found_ip}"
fi
done
if [ -z "${public_ip}" ] ; then
echo "ERROR: public IP could not be guessed." >&2
return 1
fi
echo "${public_ip}"
}
# ct_os_run_in_pod POD_NAME CMD
# --------------------
# Runs [cmd] in the pod specified by prefix [pod_prefix].
# Arguments: pod_name - full name of the pod
# Arguments: cmd - command to be run in the pod
function ct_os_run_in_pod() {
local pod_name="$1" ; shift
oc exec "$pod_name" -- "$@"
}
# ct_os_get_service_ip SERVICE_NAME
# --------------------
# Returns IP of the service specified by [service_name].
# Arguments: service_name - name of the service
function ct_os_get_service_ip() {
local service_name="${1}" ; shift
oc get "svc/${service_name}" -o yaml | grep clusterIP | \
cut -d':' -f2 | grep -oe '172\.30\.[0-9\.]*'
}
# ct_os_get_all_pods_status
# --------------------
# Returns status of all pods.
function ct_os_get_all_pods_status() {
oc get pods -o custom-columns=Ready:status.containerStatuses[0].ready,NAME:.metadata.name
}
# ct_os_get_all_pods_name
# --------------------
# Returns the full name of all pods.
function ct_os_get_all_pods_name() {
oc get pods --no-headers -o custom-columns=NAME:.metadata.name
}
# ct_os_get_pod_status POD_PREFIX
# --------------------
# Returns status of the pod specified by prefix [pod_prefix].
# Note: Ignores -build and -deploy pods
# Arguments: pod_prefix - prefix or whole ID of the pod
function ct_os_get_pod_status() {
local pod_prefix="${1}" ; shift
ct_os_get_all_pods_status | grep -e "${pod_prefix}" | grep -Ev "(build|deploy)$" \
| awk '{print $1}' | head -n 1
}
# ct_os_get_pod_name POD_PREFIX
# --------------------
# Returns the full name of pods specified by prefix [pod_prefix].
# Note: Ignores -build and -deploy pods
# Arguments: pod_prefix - prefix or whole ID of the pod
function ct_os_get_pod_name() {
local pod_prefix="${1}" ; shift
ct_os_get_all_pods_name | grep -e "^${pod_prefix}" | grep -Ev "(build|deploy)$"
}
# ct_os_get_pod_ip POD_NAME
# --------------------
# Returns the ip of the pod specified by [pod_name].
# Arguments: pod_name - full name of the pod
function ct_os_get_pod_ip() {
local pod_name="${1}"
oc get pod "$pod_name" --no-headers -o custom-columns=IP:status.podIP
}
# ct_os_check_pod_readiness POD_PREFIX STATUS
# --------------------
# Checks whether the pod is ready.
# Arguments: pod_prefix - prefix or whole ID of the pod
# Arguments: status - expected status (true, false)
function ct_os_check_pod_readiness() {
local pod_prefix="${1}" ; shift
local status="${1}" ; shift
test "$(ct_os_get_pod_status ${pod_prefix})" == "${status}"
}
# ct_os_wait_pod_ready POD_PREFIX TIMEOUT
# --------------------
# Wait maximum [timeout] for the pod becomming ready.
# Arguments: pod_prefix - prefix or whole ID of the pod
# Arguments: timeout - how many seconds to wait seconds
function ct_os_wait_pod_ready() {
local pod_prefix="${1}" ; shift
local timeout="${1}" ; shift
SECONDS=0
echo -n "Waiting for ${pod_prefix} pod becoming ready ..."
while ! ct_os_check_pod_readiness "${pod_prefix}" "true" ; do
echo -n "."
[ ${SECONDS} -gt ${timeout} ] && echo " FAIL" && return 1
sleep 3
done
echo " DONE"
}
# ct_os_wait_rc_ready POD_PREFIX TIMEOUT
# --------------------
# Wait maximum [timeout] for the rc having desired number of replicas ready.
# Arguments: pod_prefix - prefix of the replication controller
# Arguments: timeout - how many seconds to wait seconds
function ct_os_wait_rc_ready() {
local pod_prefix="${1}" ; shift
local timeout="${1}" ; shift
SECONDS=0
echo -n "Waiting for ${pod_prefix} pod becoming ready ..."
while ! test "$((oc get --no-headers statefulsets; oc get --no-headers rc) 2>/dev/null \
| grep "^${pod_prefix}" | awk '$2==$3 {print "ready"}')" == "ready" ; do
echo -n "."
[ ${SECONDS} -gt ${timeout} ] && echo " FAIL" && return 1
sleep 3
done
echo " DONE"
}
# ct_os_deploy_pure_image IMAGE [ENV_PARAMS, ...]
# --------------------
# Runs [image] in the openshift and optionally specifies env_params
# as environment variables to the image.
# Arguments: image - prefix or whole ID of the pod to run the cmd in
# Arguments: env_params - environment variables parameters for the images.
function ct_os_deploy_pure_image() {
local image="${1}" ; shift
# ignore error exit code, because oc new-app returns error when image exists
oc new-app ${image} "$@" || :
# let openshift cluster to sync to avoid some race condition errors
sleep 3
}
# ct_os_deploy_s2i_image IMAGE APP [ENV_PARAMS, ... ]
# --------------------
# Runs [image] and [app] in the openshift and optionally specifies env_params
# as environment variables to the image.
# Arguments: image - prefix or whole ID of the pod to run the cmd in
# Arguments: app - url or local path to git repo with the application sources.
# Arguments: env_params - environment variables parameters for the images.
function ct_os_deploy_s2i_image() {
local image="${1}" ; shift
local app="${1}" ; shift
# ignore error exit code, because oc new-app returns error when image exists
oc new-app "${image}~${app}" "$@" || :
# let openshift cluster to sync to avoid some race condition errors
sleep 3
}
# ct_os_deploy_template_image TEMPLATE [ENV_PARAMS, ...]
# --------------------
# Runs template in the openshift and optionally gives env_params to use
# specific values in the template.
# Arguments: template - prefix or whole ID of the pod to run the cmd in
# Arguments: env_params - environment variables parameters for the template.
# Example usage: ct_os_deploy_template_image mariadb-ephemeral-template.yaml \
# DATABASE_SERVICE_NAME=mysql-57-centos7 \
# DATABASE_IMAGE=mysql-57-centos7 \
# MYSQL_USER=testu \
# MYSQL_PASSWORD=testp \
# MYSQL_DATABASE=testdb
function ct_os_deploy_template_image() {
local template="${1}" ; shift
oc process -f "${template}" "$@" | oc create -f -
# let openshift cluster to sync to avoid some race condition errors
sleep 3
}
# _ct_os_get_uniq_project_name
# --------------------
# Returns a uniq name of the OpenShift project.
function _ct_os_get_uniq_project_name() {
local r
while true ; do
r=${RANDOM}
mkdir /var/tmp/sclorg-test-${r} &>/dev/null && echo sclorg-test-${r} && break
done
}
# ct_os_new_project [PROJECT]
# --------------------
# Creates a new project in the openshfit using 'os' command.
# Arguments: project - project name, uses a new random name if omitted
# Expects 'os' command that is properly logged in to the OpenShift cluster.
# Not using mktemp, because we cannot use uppercase characters.
function ct_os_new_project() {
if [ "${CT_SKIP_NEW_PROJECT:-false}" == 'true' ] ; then
echo "Creating project skipped."
return
fi
local project_name="${1:-$(_ct_os_get_uniq_project_name)}" ; shift || :
oc new-project ${project_name}
# let openshift cluster to sync to avoid some race condition errors
sleep 3
}
# ct_os_delete_project [PROJECT]
# --------------------
# Deletes the specified project in the openshfit
# Arguments: project - project name, uses the current project if omitted
function ct_os_delete_project() {
if [ "${CT_SKIP_NEW_PROJECT:-false}" == 'true' ] ; then
echo "Deleting project skipped, cleaning objects only."
ct_delete_all_objects
return
fi
local project_name="${1:-$(oc project -q)}" ; shift || :
oc delete project "${project_name}"
}
# ct_delete_all_objects
# -----------------
# Deletes all objects within the project.
# Handy when we have one project and want to run more tests.
function ct_delete_all_objects() {
for x in bc builds dc is isimage istag po pv pvc rc routes secrets svc ; do
oc delete $x --all
done
# for some objects it takes longer to be really deleted, so a dummy sleep
# to avoid some races when other test can see not-yet-deleted objects and can fail
sleep 10
}
# ct_os_docker_login
# --------------------
# Logs in into docker daemon
# Uses global REGISRTY_ADDRESS environment variable for arbitrary registry address.
# Does not do anything if REGISTRY_ADDRESS is set.
function ct_os_docker_login() {
[ -n "${REGISTRY_ADDRESS:-}" ] && "REGISTRY_ADDRESS set, not trying to docker login." && return 0
# docker login fails with "404 page not found" error sometimes, just try it more times
for i in `seq 12` ; do
docker login -u developer -p $(oc whoami -t) ${REGISRTY_ADDRESS:-172.30.1.1:5000} && return 0 || :
sleep 5
done
return 1
}
# ct_os_upload_image IMAGE [IMAGESTREAM]
# --------------------
# Uploads image from local registry to the OpenShift internal registry.
# Arguments: image - image name to upload
# Arguments: imagestream - name and tag to use for the internal registry.
# In the format of name:tag ($image_name:latest by default)
# Uses global REGISRTY_ADDRESS environment variable for arbitrary registry address.
function ct_os_upload_image() {
local input_name="${1}" ; shift
local image_name=${input_name##*/}
local imagestream=${1:-$image_name:latest}
local output_name="${REGISRTY_ADDRESS:-172.30.1.1:5000}/$(oc project -q)/$imagestream"
ct_os_docker_login
docker tag ${input_name} ${output_name}
docker push ${output_name}
}
# ct_os_install_in_centos
# --------------------
# Installs os cluster in CentOS
function ct_os_install_in_centos() {
yum install -y centos-release-openshift-origin
yum install -y wget git net-tools bind-utils iptables-services bridge-utils\
bash-completion origin-clients docker origin-clients
}
# ct_os_cluster_up [DIR, IS_PUBLIC, CLUSTER_VERSION]
# --------------------
# Runs the local OpenShift cluster using 'oc cluster up' and logs in as developer.
# Arguments: dir - directory to keep configuration data in, random if omitted
# Arguments: is_public - sets either private or public hostname for web-UI,
# use "true" for allow remote access to the web-UI,
# "false" is default
# Arguments: cluster_version - version of the OpenShift cluster to use, empty
# means default version of `oc`; example value: 3.7;
# also can be specified outside by OC_CLUSTER_VERSION
function ct_os_cluster_up() {
ct_os_cluster_running && echo "Cluster already running. Nothing is done." && return 0
ct_os_logged_in && echo "Already logged in to a cluster. Nothing is done." && return 0
mkdir -p /var/tmp/openshift
local dir="${1:-$(mktemp -d /var/tmp/openshift/os-data-XXXXXX)}" ; shift || :
local is_public="${1:-'false'}" ; shift || :
local default_cluster_version=${OC_CLUSTER_VERSION:-}
local cluster_version=${1:-${default_cluster_version}} ; shift || :
if ! grep -qe '--insecure-registry.*172\.30\.0\.0' /etc/sysconfig/docker ; then
sed -i "s|OPTIONS='|OPTIONS='--insecure-registry 172.30.0.0/16 |" /etc/sysconfig/docker
fi
systemctl stop firewalld || :
setenforce 0
iptables -F
systemctl restart docker
local cluster_ip="127.0.0.1"
[ "${is_public}" == "true" ] && cluster_ip=$(ct_get_public_ip)
if [ -n "${cluster_version}" ] ; then
# if $cluster_version is not set, we simply use oc that is available
ct_os_set_path_oc "${cluster_version}"
fi
mkdir -p ${dir}/{config,data,pv}
case $(oc version| head -n 1) in
"oc v3.1"?.*)
oc cluster up --base-dir="${dir}/data" --public-hostname="${cluster_ip}"
;;
"oc v3."*)
oc cluster up --host-data-dir="${dir}/data" --host-config-dir="${dir}/config" \
--host-pv-dir="${dir}/pv" --use-existing-config --public-hostname="${cluster_ip}"
;;
*)
echo "ERROR: Unexpected oc version." >&2
return 1
;;
esac
oc version
oc login -u system:admin
oc project default
ct_os_wait_rc_ready docker-registry 180
ct_os_wait_rc_ready router 30
oc login -u developer -p developer
# let openshift cluster to sync to avoid some race condition errors
sleep 3
}
# ct_os_cluster_down
# --------------------
# Shuts down the local OpenShift cluster using 'oc cluster down'
function ct_os_cluster_down() {
oc cluster down
}
# ct_os_cluster_running
# --------------------
# Returns 0 if oc cluster is running
function ct_os_cluster_running() {
oc cluster status &>/dev/null
}
# ct_os_logged_in
# ---------------
# Returns 0 if logged in to a cluster (remote or local)
function ct_os_logged_in() {
oc whoami >/dev/null
}
# ct_os_set_path_oc OC_VERSION
# --------------------
# This is a trick that helps using correct version of the `oc`:
# The input is version of the openshift in format v3.6.0 etc.
# If the currently available version of oc is not of this version,
# it first takes a look into /usr/local/oc-<ver>/bin directory,
# and if not found there it downloads the community release from github.
# In the end the PATH variable is changed, so the other tests can still use just 'oc'.
# Arguments: oc_version - X.Y part of the version of OSE (e.g. 3.9)
function ct_os_set_path_oc() {
local oc_version=$(ct_os_get_latest_ver $1)
local oc_path
if oc version | grep -q "oc ${oc_version%.*}." ; then
echo "Binary oc found already available in version ${oc_version}: `which oc` Doing noting."
return 0
fi
# first check whether we already have oc available in /usr/local
local installed_oc_path="/usr/local/oc-${oc_version%.*}/bin"
if [ -x "${installed_oc_path}/oc" ] ; then
oc_path="${installed_oc_path}"
echo "Binary oc found in ${installed_oc_path}" >&2
else
# oc not available in /usr/local, try to download it from github (community release)
oc_path="/tmp/oc-${oc_version}-bin"
ct_os_download_upstream_oc "${oc_version}" "${oc_path}"
fi
if [ -z "${oc_path}/oc" ] ; then
echo "ERROR: oc not found installed, nor downloaded" >&1
return 1
fi
export PATH="${oc_path}:${PATH}"
if ! oc version | grep -q "oc ${oc_version%.*}." ; then
echo "ERROR: something went wrong, oc located at ${oc_path}, but oc of version ${oc_version} not found in PATH ($PATH)" >&1
return 1
else
echo "PATH set correctly, binary oc found in version ${oc_version}: `which oc`"
fi
}
# ct_os_get_latest_ver VERSION_PART_X
# --------------------
# Returns full version (vX.Y.Z) from part of the version (X.Y)
# Arguments: vxy - X.Y part of the version
# Returns vX.Y.Z variant of the version
function ct_os_get_latest_ver(){
local vxy="v$1"
for vz in {3..0} ; do
curl -sif "https://github.com/openshift/origin/releases/tag/${vxy}.${vz}" >/dev/null && echo "${vxy}.${vz}" && return 0
done
echo "ERROR: version ${vxy} not found in https://github.com/openshift/origin/tags" >&2
return 1
}
# ct_os_download_upstream_oc OC_VERSION OUTPUT_DIR
# --------------------
# Downloads a particular version of openshift-origin-client-tools from
# github into specified output directory
# Arguments: oc_version - version of OSE (e.g. v3.7.2)
# Arguments: output_dir - output directory
function ct_os_download_upstream_oc() {
local oc_version=$1
local output_dir=$2
# check whether we already have the binary in place
[ -x "${output_dir}/oc" ] && return 0
mkdir -p "${output_dir}"
# using html output instead of https://api.github.com/repos/openshift/origin/releases/tags/${oc_version},
# because API is limited for number of queries if not authenticated
tarball=$(curl -si "https://github.com/openshift/origin/releases/tag/${oc_version}" | grep -o -e "openshift-origin-client-tools-${oc_version}-[a-f0-9]*-linux-64bit.tar.gz" | head -n 1)
# download, unpack the binaries and then put them into output directory
echo "Downloading https://github.com/openshift/origin/releases/download/${oc_version}/${tarball} into ${output_dir}/" >&2
curl -sL https://github.com/openshift/origin/releases/download/${oc_version}/"${tarball}" | tar -C "${output_dir}" -xz
mv -f "${output_dir}"/"${tarball%.tar.gz}"/* "${output_dir}/"
rmdir "${output_dir}"/"${tarball%.tar.gz}"
}
# ct_os_test_s2i_app_func IMAGE APP CONTEXT_DIR CHECK_CMD [OC_ARGS]
# --------------------
# Runs [image] and [app] in the openshift and optionally specifies env_params
# as environment variables to the image. Then check the container by arbitrary
# function given as argument (such an argument may include <IP> string,
# that will be replaced with actual IP).
# Arguments: image - prefix or whole ID of the pod to run the cmd in (compulsory)
# Arguments: app - url or local path to git repo with the application sources (compulsory)
# Arguments: context_dir - sub-directory inside the repository with the application sources (compulsory)
# Arguments: check_command - CMD line that checks whether the container works (compulsory; '<IP>' will be replaced with actual IP)
# Arguments: oc_args - all other arguments are used as additional parameters for the `oc new-app`
# command, typically environment variables (optional)
function ct_os_test_s2i_app_func() {
local image_name=${1}
local app=${2}
local context_dir=${3}
local check_command=${4}
local oc_args=${5:-}
local import_image=${6:-}
local image_name_no_namespace=${image_name##*/}
local service_name="${image_name_no_namespace}-testing"
local image_tagged="${image_name_no_namespace}:${VERSION}"
if [ $# -lt 4 ] || [ -z "${1}" -o -z "${2}" -o -z "${3}" -o -z "${4}" ]; then
echo "ERROR: ct_os_test_s2i_app_func() requires at least 4 arguments that cannot be emtpy." >&2
return 1
fi
ct_os_new_project
# Create a specific imagestream tag for the image so that oc cannot use anything else
if [ "${CT_SKIP_UPLOAD_IMAGE:-false}" == 'true' ] ; then
if [ -n "${import_image}" ] ; then
echo "Importing image ${import_image} as ${image_name}:${VERSION}"
oc import-image ${image_name}:${VERSION} --from ${import_image} --confirm
else
echo "Uploading and importing image skipped."
fi
else
if [ -n "${import_image}" ] ; then
echo "Warning: Import image ${import_image} requested, but uploading image ${image_name} instead."
fi
ct_os_upload_image "${image_name}" "${image_tagged}"
fi
local app_param="${app}"
if [ -d "${app}" ] ; then
# for local directory, we need to copy the content, otherwise too smart os command
# pulls the git remote repository instead
app_param=$(ct_obtain_input "${app}")
fi
ct_os_deploy_s2i_image "${image_tagged}" "${app_param}" \
--context-dir="${context_dir}" \
--name "${service_name}" \
${oc_args}
if [ -d "${app}" ] ; then
# in order to avoid weird race seen sometimes, let's wait shortly
# before starting the build explicitly
sleep 5
oc start-build "${service_name}" --from-dir="${app_param}"
fi
ct_os_wait_pod_ready "${service_name}" 300
local ip=$(ct_os_get_service_ip "${service_name}")
local check_command_exp=$(echo "$check_command" | sed -e "s/<IP>/$ip/g")
echo " Checking APP using $check_command_exp ..."
local result=0
eval "$check_command_exp" || result=1
if [ $result -eq 0 ] ; then
echo " Check passed."
else
echo " Check failed."
fi
ct_os_delete_project
return $result
}
# ct_os_test_s2i_app IMAGE APP CONTEXT_DIR EXPECTED_OUTPUT [PORT, PROTOCOL, RESPONSE_CODE, OC_ARGS, ... ]
# --------------------
# Runs [image] and [app] in the openshift and optionally specifies env_params
# as environment variables to the image. Then check the http response.
# Arguments: image - prefix or whole ID of the pod to run the cmd in (compulsory)
# Arguments: app - url or local path to git repo with the application sources (compulsory)
# Arguments: context_dir - sub-directory inside the repository with the application sources (compulsory)
# Arguments: expected_output - PCRE regular expression that must match the response body (compulsory)
# Arguments: port - which port to use (optional; default: 8080)
# Arguments: protocol - which protocol to use (optional; default: http)
# Arguments: response_code - what http response code to expect (optional; default: 200)
# Arguments: oc_args - all other arguments are used as additional parameters for the `oc new-app`
# command, typically environment variables (optional)
function ct_os_test_s2i_app() {
local image_name=${1}
local app=${2}
local context_dir=${3}
local expected_output=${4}
local port=${5:-8080}
local protocol=${6:-http}
local response_code=${7:-200}
local oc_args=${8:-}
local import_image=${9:-}
if [ $# -lt 4 ] || [ -z "${1}" -o -z "${2}" -o -z "${3}" -o -z "${4}" ]; then
echo "ERROR: ct_os_test_s2i_app() requires at least 4 arguments that cannot be emtpy." >&2
return 1
fi
ct_os_test_s2i_app_func "${image_name}" \
"${app}" \
"${context_dir}" \
"ct_os_test_response_internal '${protocol}://<IP>:${port}' '${response_code}' '${expected_output}'" \
"${oc_args}" "${import_image}"
}
# ct_os_test_template_app_func IMAGE APP IMAGE_IN_TEMPLATE CHECK_CMD [OC_ARGS]
# --------------------
# Runs [image] and [app] in the openshift and optionally specifies env_params
# as environment variables to the image. Then check the container by arbitrary
# function given as argument (such an argument may include <IP> string,
# that will be replaced with actual IP).
# Arguments: image_name - prefix or whole ID of the pod to run the cmd in (compulsory)
# Arguments: template - url or local path to a template to use (compulsory)
# Arguments: name_in_template - image name used in the template
# Arguments: check_command - CMD line that checks whether the container works (compulsory; '<IP>' will be replaced with actual IP)
# Arguments: oc_args - all other arguments are used as additional parameters for the `oc new-app`
# command, typically environment variables (optional)
# Arguments: other_images - some templates need other image to be pushed into the OpenShift registry,
# specify them in this parameter as "<image>|<tag>", where "<image>" is a full image name
# (including registry if needed) and "<tag>" is a tag under which the image should be available
# in the OpenShift registry.
function ct_os_test_template_app_func() {
local image_name=${1}
local template=${2}
local name_in_template=${3}
local check_command=${4}
local oc_args=${5:-}
local other_images=${6:-}
local import_image=${7:-}
if [ $# -lt 4 ] || [ -z "${1}" -o -z "${2}" -o -z "${3}" -o -z "${4}" ]; then
echo "ERROR: ct_os_test_template_app_func() requires at least 4 arguments that cannot be emtpy." >&2
return 1
fi
local service_name="${name_in_template}-testing"
local image_tagged="${name_in_template}:${VERSION}"
ct_os_new_project
# Create a specific imagestream tag for the image so that oc cannot use anything else
if [ "${CT_SKIP_UPLOAD_IMAGE:-false}" == 'true' ] ; then
if [ -n "${import_image}" ] ; then
echo "Importing image ${import_image} as ${image_name}:${VERSION}"
oc import-image ${image_name}:${VERSION} --from ${import_image} --confirm
else
echo "Uploading and importing image skipped."
fi
else
if [ -n "${import_image}" ] ; then
echo "Warning: Import image ${import_image} requested, but uploading image ${image_name} instead."
fi
ct_os_upload_image "${image_name}" "${image_tagged}"
# upload also other images, that template might need (list of pairs in the format <image>|<tag>
local images_tags_a
local i_t
for i_t in ${other_images} ; do
echo "${i_t}"
IFS='|' read -ra image_tag_a <<< "${i_t}"
docker pull "${image_tag_a[0]}"
ct_os_upload_image "${image_tag_a[0]}" "${image_tag_a[1]}"
done
fi
local local_template=$(ct_obtain_input "${template}")
local namespace=${CT_NAMESPACE:-$(oc project -q)}
oc new-app ${local_template} \
--name "${name_in_template}" \
-p NAMESPACE="${namespace}" \
${oc_args}
ct_os_wait_pod_ready "${service_name}" 300
local ip=$(ct_os_get_service_ip "${service_name}")
local check_command_exp=$(echo "$check_command" | sed -e "s/<IP>/$ip/g")
echo " Checking APP using $check_command_exp ..."
local result=0
eval "$check_command_exp" || result=1
if [ $result -eq 0 ] ; then
echo " Check passed."
else
echo " Check failed."
fi
ct_os_delete_project
return $result
}
# params:
# ct_os_test_template_app IMAGE APP IMAGE_IN_TEMPLATE EXPECTED_OUTPUT [PORT, PROTOCOL, RESPONSE_CODE, OC_ARGS, ... ]
# --------------------
# Runs [image] and [app] in the openshift and optionally specifies env_params
# as environment variables to the image. Then check the http response.
# Arguments: image_name - prefix or whole ID of the pod to run the cmd in (compulsory)
# Arguments: template - url or local path to a template to use (compulsory)
# Arguments: name_in_template - image name used in the template
# Arguments: expected_output - PCRE regular expression that must match the response body (compulsory)
# Arguments: port - which port to use (optional; default: 8080)
# Arguments: protocol - which protocol to use (optional; default: http)
# Arguments: response_code - what http response code to expect (optional; default: 200)
# Arguments: oc_args - all other arguments are used as additional parameters for the `oc new-app`
# command, typically environment variables (optional)
# Arguments: other_images - some templates need other image to be pushed into the OpenShift registry,
# specify them in this parameter as "<image>|<tag>", where "<image>" is a full image name
# (including registry if needed) and "<tag>" is a tag under which the image should be available
# in the OpenShift registry.
function ct_os_test_template_app() {
local image_name=${1}
local template=${2}
local name_in_template=${3}
local expected_output=${4}
local port=${5:-8080}
local protocol=${6:-http}
local response_code=${7:-200}
local oc_args=${8:-}
local other_images=${9:-}
local import_image=${10:-}
if [ $# -lt 4 ] || [ -z "${1}" -o -z "${2}" -o -z "${3}" -o -z "${4}" ]; then
echo "ERROR: ct_os_test_template_app() requires at least 4 arguments that cannot be emtpy." >&2
return 1
fi
ct_os_test_template_app_func "${image_name}" \
"${template}" \
"${name_in_template}" \
"ct_os_test_response_internal '${protocol}://<IP>:${port}' '${response_code}' '${expected_output}'" \
"${oc_args}" \
"${other_images}" \
"${import_image}"
}
# ct_os_test_image_update IMAGE_NAME OLD_IMAGE ISTAG CHECK_FUNCTION OC_ARGS
# --------------------
# Runs an image update test with [image] uploaded to [is] imagestream
# and checks the services using an arbitrary function provided in [check_function].
# Arguments: image_name - prefix or whole ID of the pod to run the cmd in (compulsory)
# Arguments: old_image - valid name of the image from the registry
# Arguments: istag - imagestream to upload the images into (compulsory)
# Arguments: check_function - command to be run to check functionality of created services (compulsory)
# Arguments: oc_args - arguments to use during oc new-app (compulsory)
ct_os_test_image_update() {
local image_name=$1; shift
local old_image=$1; shift
local istag=$1; shift
local check_function=$1; shift
local service_name=${image_name##*/}
local ip="" check_command_exp=""
echo "Running image update test for: $image_name"
ct_os_new_project
# Get current image from repository and create an imagestream
docker pull "$old_image:latest" 2>/dev/null
ct_os_upload_image "$old_image" "$istag"
# Setup example application with curent image
oc new-app "$@" --name "$service_name"
ct_os_wait_pod_ready "$service_name" 60
# Check application output
ip=$(ct_os_get_service_ip "$service_name")
check_command_exp=${check_function//<IP>/$ip}
ct_assert_cmd_success "$check_command_exp"
# Tag built image into the imagestream and wait for rebuild
ct_os_upload_image "$image_name" "$istag"
ct_os_wait_pod_ready "${service_name}-2" 60
# Check application output
ip=$(ct_os_get_service_ip "$service_name")
check_command_exp=${check_function//<IP>/$ip}
ct_assert_cmd_success "$check_command_exp"
ct_os_delete_project
}
# ct_os_deploy_cmd_image IMAGE_NAME
# --------------------
# Runs a special command pod, a pod that does nothing, but includes utilities for testing.
# A typical usage is a mysql pod that includes mysql commandline, that we need for testing.
# Running commands inside this command pod is done via ct_os_cmd_image_run function.
# The pod is not run again if already running.
# Arguments: image_name - image to be used as a command pod
function ct_os_deploy_cmd_image() {
local image_name=${1}
oc get pod command-app &>/dev/null && echo "command POD already running" && return 0
echo "command POD not running yet, will start one called command-app"
oc create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: command-app
spec:
containers:
- name: command-container
image: "${image_name}"
command: ["sleep"]
args: ["3h"]
restartPolicy: OnFailure
EOF
SECONDS=0
echo -n "Waiting for command POD ."
while [ $SECONDS -lt 180 ] ; do
sout="$(ct_os_cmd_image_run 'echo $((11*11))')"
grep -q '^121$' <<< "$sout" && echo "DONE" && return 0 || :
sleep 3
echo -n "."
done
echo "FAIL"
return 1
}
# ct_os_cmd_image_run CMD [ ARG ... ]
# --------------------
# Runs a command CMD inside a special command pod
# Arguments: cmd - shell command with args to run in a pod
function ct_os_cmd_image_run() {
oc exec command-app -- bash -c "$@"
}
# ct_os_test_response_internal
# ----------------
# Perform GET request to the application container, checks output with
# a reg-exp and HTTP response code.
# That all is done inside an image in the cluster, so the function is used
# typically in clusters that are not accessible outside.
# The interanal image is a python image that should include the most of the useful commands.
# The check is repeated until timeout.
# Argument: url - request URL path
# Argument: expected_code - expected HTTP response code
# Argument: body_regexp - PCRE regular expression that must match the response body
# Argument: max_attempts - Optional number of attempts (default: 20), three seconds sleep between
# Argument: ignore_error_attempts - Optional number of attempts when we ignore error output (default: 10)
ct_os_test_response_internal() {
local url="$1"
local expected_code="$2"
local body_regexp="$3"
local max_attempts=${4:-20}
local ignore_error_attempts=${5:-10}
: " Testing the HTTP(S) response for <${url}>"
local sleep_time=3
local attempt=1
local result=1
local status
local response_code
local response_file=$(mktemp /tmp/ct_test_response_XXXXXX)
local util_image_name='python:3.6'
ct_os_deploy_cmd_image "${util_image_name}"
while [ ${attempt} -le ${max_attempts} ]; do
ct_os_cmd_image_run "curl --connect-timeout 10 -s -w '%{http_code}' '${url}'" >${response_file} && status=0 || status=1
if [ ${status} -eq 0 ]; then
response_code=$(cat ${response_file} | tail -c 3)
if [ "${response_code}" -eq "${expected_code}" ]; then
result=0
fi
cat ${response_file} | grep -qP -e "${body_regexp}" || result=1;
# Some services return 40x code until they are ready, so let's give them
# some chance and not end with failure right away
# Do not wait if we already have expected outcome though
if [ ${result} -eq 0 -o ${attempt} -gt ${ignore_error_attempts} -o ${attempt} -eq ${max_attempts} ] ; then
break
fi
fi
attempt=$(( ${attempt} + 1 ))
sleep ${sleep_time}
done
rm -f ${response_file}
return ${result}
}
# ct_os_get_image_from_pod
# ------------------------
# Print image identifier from an existing pod to stdout
# Argument: pod_prefix - prefix or full name of the pod to get image from
ct_os_get_image_from_pod() {
local pod_prefix=$1 ; shift
local pod_name=$(ct_os_get_pod_name $pod_prefix)
oc get "po/${pod_name}" -o yaml | sed -ne 's/^\s*image:\s*\(.*\)\s*$/\1/ p' | head -1
}
# ct_os_check_cmd_internal
# ----------------
# Runs a specified command, checks exit code and compares the output with expected regexp.
# That all is done inside an image in the cluster, so the function is used
# typically in clusters that are not accessible outside.
# The check is repeated until timeout.
# Argument: util_image_name - name of the image in the cluster that is used for running the cmd
# Argument: service_name - kubernetes' service name to work with (IP address is taken from this one)
# Argument: check_command - command that is run within the util_image_name container
# Argument: expected_content_match - regexp that must be in the output (use .* to ignore check)
# Argument: timeout - number of seconds to wait till the check succeeds
function ct_os_check_cmd_internal() {
local util_image_name=$1 ; shift
local service_name=$1 ; shift
local check_command=$1 ; shift
local expected_content_match=${1:-.*} ; shift
local timeout=${1:-60} ; shift || :
: " Service ${service_name} check ..."
local output
local ret
local ip=$(ct_os_get_service_ip "${service_name}")
local check_command_exp=$(echo "$check_command" | sed -e "s/<IP>/$ip/g")
ct_os_deploy_cmd_image $(ct_os_get_image_from_pod "${util_image_name}" | head -n 1)
SECONDS=0
echo -n "Waiting for ${service_name} service becoming ready ..."
while true ; do
output=$(ct_os_cmd_image_run "$check_command_exp")
ret=$?
echo "${output}" | grep -qe "${expected_content_match}" || ret=1
if [ ${ret} -eq 0 ] ; then
echo " PASS"
return 0
fi
echo -n "."
[ ${SECONDS} -gt ${timeout} ] && break
sleep 3
done
echo " FAIL"
return 1
}

507
test/test-lib.sh Normal file
View file

@ -0,0 +1,507 @@
#
# Test a container image.
#
# Always use sourced from a specific container testfile
#
# reguires definition of CID_FILE_DIR
# CID_FILE_DIR=$(mktemp --suffix=<container>_test_cidfiles -d)
# reguires definition of TEST_LIST
# TEST_LIST="\
# ctest_container_creation
# ctest_doc_content"
# Container CI tests
# abbreviated as "ct"
# may be redefined in the specific container testfile
EXPECTED_EXIT_CODE=0
# ct_cleanup
# --------------------
# Cleans up containers used during tests. Stops and removes all containers
# referenced by cid_files in CID_FILE_DIR. Dumps logs if a container exited
# unexpectedly. Removes the cid_files and CID_FILE_DIR as well.
# Uses: $CID_FILE_DIR - path to directory containing cid_files
# Uses: $EXPECTED_EXIT_CODE - expected container exit code
function ct_cleanup() {
for cid_file in $CID_FILE_DIR/* ; do
local container=$(cat $cid_file)
: "Stopping and removing container $container..."
docker stop $container
exit_status=$(docker inspect -f '{{.State.ExitCode}}' $container)
if [ "$exit_status" != "$EXPECTED_EXIT_CODE" ]; then
: "Dumping logs for $container"
docker logs $container
fi
docker rm -v $container
rm $cid_file
done
rmdir $CID_FILE_DIR
: "Done."
}
# ct_enable_cleanup
# --------------------
# Enables automatic container cleanup after tests.
function ct_enable_cleanup() {
trap ct_cleanup EXIT SIGINT
}
# ct_get_cid [name]
# --------------------
# Prints container id from cid_file based on the name of the file.
# Argument: name - name of cid_file where the container id will be stored
# Uses: $CID_FILE_DIR - path to directory containing cid_files
function ct_get_cid() {
local name="$1" ; shift || return 1
echo $(cat "$CID_FILE_DIR/$name")
}
# ct_get_cip [id]
# --------------------
# Prints container ip address based on the container id.
# Argument: id - container id
function ct_get_cip() {
local id="$1" ; shift
docker inspect --format='{{.NetworkSettings.IPAddress}}' $(ct_get_cid "$id")
}
# ct_wait_for_cid [cid_file]
# --------------------
# Holds the execution until the cid_file is created. Usually run after container
# creation.
# Argument: cid_file - name of the cid_file that should be created
function ct_wait_for_cid() {
local cid_file=$1
local max_attempts=10
local sleep_time=1
local attempt=1
local result=1
while [ $attempt -le $max_attempts ]; do
[ -f $cid_file ] && [ -s $cid_file ] && return 0
: "Waiting for container start..."
attempt=$(( $attempt + 1 ))
sleep $sleep_time
done
return 1
}
# ct_assert_container_creation_fails [container_args]
# --------------------
# The invocation of docker run should fail based on invalid container_args
# passed to the function. Returns 0 when container fails to start properly.
# Argument: container_args - all arguments are passed directly to dokcer run
# Uses: $CID_FILE_DIR - path to directory containing cid_files
function ct_assert_container_creation_fails() {
local ret=0
local max_attempts=10
local attempt=1
local cid_file=assert
set +e
local old_container_args="${CONTAINER_ARGS-}"
CONTAINER_ARGS="$@"
ct_create_container $cid_file
if [ $? -eq 0 ]; then
local cid=$(ct_get_cid $cid_file)
while [ "$(docker inspect -f '{{.State.Running}}' $cid)" == "true" ] ; do
sleep 2
attempt=$(( $attempt + 1 ))
if [ $attempt -gt $max_attempts ]; then
docker stop $cid
ret=1
break
fi
done
exit_status=$(docker inspect -f '{{.State.ExitCode}}' $cid)
if [ "$exit_status" == "0" ]; then
ret=1
fi
docker rm -v $cid
rm $CID_FILE_DIR/$cid_file
fi
[ ! -z $old_container_args ] && CONTAINER_ARGS="$old_container_args"
set -e
return $ret
}
# ct_create_container [name, command]
# --------------------
# Creates a container using the IMAGE_NAME and CONTAINER_ARGS variables. Also
# stores the container id to a cid_file located in the CID_FILE_DIR, and waits
# for the creation of the file.
# Argument: name - name of cid_file where the container id will be stored
# Argument: command - optional command to be executed in the container
# Uses: $CID_FILE_DIR - path to directory containing cid_files
# Uses: $CONTAINER_ARGS - optional arguments passed directly to docker run
# Uses: $IMAGE_NAME - name of the image being tested
function ct_create_container() {
local cid_file="$CID_FILE_DIR/$1" ; shift
# create container with a cidfile in a directory for cleanup
docker run --cidfile="$cid_file" -d ${CONTAINER_ARGS:-} $IMAGE_NAME "$@"
ct_wait_for_cid $cid_file || return 1
: "Created container $(cat $cid_file)"
}
# ct_scl_usage_old [name, command, expected]
# --------------------
# Tests three ways of running the SCL, by looking for an expected string
# in the output of the command
# Argument: name - name of cid_file where the container id will be stored
# Argument: command - executed inside the container
# Argument: expected - string that is expected to be in the command output
# Uses: $CID_FILE_DIR - path to directory containing cid_files
# Uses: $IMAGE_NAME - name of the image being tested
function ct_scl_usage_old() {
local name="$1"
local command="$2"
local expected="$3"
local out=""
: " Testing the image SCL enable"
out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${command}")
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[/bin/bash -c "${command}"] Expected '${expected}', got '${out}'" >&2
return 1
fi
out=$(docker exec $(ct_get_cid $name) /bin/bash -c "${command}" 2>&1)
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[exec /bin/bash -c "${command}"] Expected '${expected}', got '${out}'" >&2
return 1
fi
out=$(docker exec $(ct_get_cid $name) /bin/sh -ic "${command}" 2>&1)
if ! echo "${out}" | grep -q "${expected}"; then
echo "ERROR[exec /bin/sh -ic "${command}"] Expected '${expected}', got '${out}'" >&2
return 1
fi
}
# ct_doc_content_old [strings]
# --------------------
# Looks for occurence of stirngs in the documentation files and checks
# the format of the files. Files examined: help.1
# Argument: strings - strings expected to appear in the documentation
# Uses: $IMAGE_NAME - name of the image being tested
function ct_doc_content_old() {
local tmpdir=$(mktemp -d)
local f
: " Testing documentation in the container image"
# Extract the help files from the container
for f in help.1 ; do
docker run --rm ${IMAGE_NAME} /bin/bash -c "cat /${f}" >${tmpdir}/$(basename ${f})
# Check whether the files contain some important information
for term in $@ ; do
if ! cat ${tmpdir}/$(basename ${f}) | grep -F -q -e "${term}" ; then
echo "ERROR: File /${f} does not include '${term}'." >&2
return 1
fi
done
# Check whether the files use the correct format
for term in TH PP SH ; do
if ! grep -q "^\.${term}" ${tmpdir}/help.1 ; then
echo "ERROR: /help.1 is probably not in troff or groff format, since '${term}' is missing." >&2
return 1
fi
done
done
: " Success!"
}
# ct_npm_works
# --------------------
# Checks existance of the npm tool and runs it.
function ct_npm_works() {
local tmpdir=$(mktemp -d)
: " Testing npm in the container image"
docker run --rm ${IMAGE_NAME} /bin/bash -c "npm --version" >${tmpdir}/version
if [ $? -ne 0 ] ; then
echo "ERROR: 'npm --version' does not work inside the image ${IMAGE_NAME}." >&2
return 1
fi
docker run --rm ${IMAGE_NAME} /bin/bash -c "npm install jquery && test -f node_modules/jquery/src/jquery.js"
if [ $? -ne 0 ] ; then
echo "ERROR: npm could not install jquery inside the image ${IMAGE_NAME}." >&2
return 1
fi
: " Success!"
}
# ct_path_append PATH_VARNAME DIRECTORY
# -------------------------------------
# Append DIRECTORY to VARIABLE of name PATH_VARNAME, the VARIABLE must consist
# of colon-separated list of directories.
ct_path_append ()
{
if eval "test -n \"\${$1-}\""; then
eval "$1=\$2:\$$1"
else
eval "$1=\$2"
fi
}
# ct_path_foreach PATH ACTION [ARGS ...]
# --------------------------------------
# For each DIR in PATH execute ACTION (path is colon separated list of
# directories). The particular calls to ACTION will look like
# '$ ACTION directory [ARGS ...]'
ct_path_foreach ()
{
local dir dirlist action save_IFS
save_IFS=$IFS
IFS=:
dirlist=$1
action=$2
shift 2
for dir in $dirlist; do "$action" "$dir" "$@" ; done
IFS=$save_IFS
}
# ct_run_test_list
# --------------------
# Execute the tests specified by TEST_LIST
# Uses: $TEST_LIST - list of test names
function ct_run_test_list() {
for test_case in $TEST_LIST; do
: "Running test $test_case"
[ -f test/$test_case ] && source test/$test_case
[ -f ../test/$test_case ] && source ../test/$test_case
$test_case
done;
}
# ct_gen_self_signed_cert_pem
# ---------------------------
# Generates a self-signed PEM certificate pair into specified directory.
# Argument: output_dir - output directory path
# Argument: base_name - base name of the certificate files
# Resulted files will be those:
# <output_dir>/<base_name>-cert-selfsigned.pem -- public PEM cert
# <output_dir>/<base_name>-key.pem -- PEM private key
ct_gen_self_signed_cert_pem() {
local output_dir=$1 ; shift
local base_name=$1 ; shift
mkdir -p ${output_dir}
openssl req -newkey rsa:2048 -nodes -keyout ${output_dir}/${base_name}-key.pem -subj '/C=GB/ST=Berkshire/L=Newbury/O=My Server Company' > ${base_name}-req.pem
openssl req -new -x509 -nodes -key ${output_dir}/${base_name}-key.pem -batch > ${output_dir}/${base_name}-cert-selfsigned.pem
}
# ct_obtain_input FILE|DIR|URL
# --------------------
# Either copies a file or a directory to a tmp location for local copies, or
# downloads the file from remote location.
# Resulted file path is printed, so it can be later used by calling function.
# Arguments: input - local file, directory or remote URL
function ct_obtain_input() {
local input=$1
local extension="${input##*.}"
# Try to use same extension for the temporary file if possible
[[ "${extension}" =~ ^[a-z0-9]*$ ]] && extension=".${extension}" || extension=""
local output=$(mktemp "/var/tmp/test-input-XXXXXX$extension")
if [ -f "${input}" ] ; then
cp -f "${input}" "${output}"
elif [ -d "${input}" ] ; then
rm -f "${output}"
cp -r -LH "${input}" "${output}"
elif echo "${input}" | grep -qe '^http\(s\)\?://' ; then
curl "${input}" > "${output}"
else
echo "ERROR: file type not known: ${input}" >&2
return 1
fi
echo "${output}"
}
# ct_test_response
# ----------------
# Perform GET request to the application container, checks output with
# a reg-exp and HTTP response code.
# Argument: url - request URL path
# Argument: expected_code - expected HTTP response code
# Argument: body_regexp - PCRE regular expression that must match the response body
# Argument: max_attempts - Optional number of attempts (default: 20), three seconds sleep between
# Argument: ignore_error_attempts - Optional number of attempts when we ignore error output (default: 10)
ct_test_response() {
local url="$1"
local expected_code="$2"
local body_regexp="$3"
local max_attempts=${4:-20}
local ignore_error_attempts=${5:-10}
: " Testing the HTTP(S) response for <${url}>"
local sleep_time=3
local attempt=1
local result=1
local status
local response_code
local response_file=$(mktemp /tmp/ct_test_response_XXXXXX)
while [ ${attempt} -le ${max_attempts} ]; do
curl --connect-timeout 10 -s -w '%{http_code}' "${url}" >${response_file} && status=0 || status=1
if [ ${status} -eq 0 ]; then
response_code=$(cat ${response_file} | tail -c 3)
if [ "${response_code}" -eq "${expected_code}" ]; then
result=0
fi
cat ${response_file} | grep -qP -e "${body_regexp}" || result=1;
# Some services return 40x code until they are ready, so let's give them
# some chance and not end with failure right away
# Do not wait if we already have expected outcome though
if [ ${result} -eq 0 -o ${attempt} -gt ${ignore_error_attempts} -o ${attempt} -eq ${max_attempts} ] ; then
break
fi
fi
attempt=$(( ${attempt} + 1 ))
sleep ${sleep_time}
done
rm -f ${response_file}
return ${result}
}
# ct_registry_from_os OS
# ----------------
# Transform operating system string [os] into registry url
# Argument: OS - string containing the os version
ct_registry_from_os() {
local registry=""
case $1 in
rhel7)
registry=registry.access.redhat.com
;;
*)
registry=docker.io
;;
esac
echo "$registry"
}
# ct_assert_cmd_success CMD
# ----------------
# Evaluates [cmd] and fails if it does not succeed.
# Argument: CMD - Command to be run
function ct_assert_cmd_success() {
echo "Checking '$*' for success ..."
if ! eval "$@" &>/dev/null; then
echo " FAIL"
return 1
fi
echo " PASS"
return 0
}
# ct_assert_cmd_failure CMD
# ----------------
# Evaluates [cmd] and fails if it succeeds.
# Argument: CMD - Command to be run
function ct_assert_cmd_failure() {
echo "Checking '$*' for failure ..."
if eval "$@" &>/dev/null; then
echo " FAIL"
return 1
fi
echo " PASS"
return 0
}
# ct_random_string [LENGTH=10]
# ----------------------------
# Generate pseudorandom alphanumeric string of LENGTH bytes, the
# default length is 10. The string is printed on stdout.
ct_random_string()
(
export LC_ALL=C
dd if=/dev/urandom count=1 bs=10k 2>/dev/null \
| tr -dc 'a-z0-9' \
| fold -w "${1-10}" \
| head -n 1
)
# ct_s2i_usage IMG_NAME [S2I_ARGS]
# ----------------------------
# Create a container and run the usage script inside
# Argument: IMG_NAME - name of the image to be used for the container run
# Argument: S2I_ARGS - Additional list of source-to-image arguments, currently unused.
ct_s2i_usage()
{
local img_name=$1; shift
local s2i_args="$*";
local usage_command="/usr/libexec/s2i/usage"
docker run --rm "$img_name" bash -c "$usage_command"
}
# ct_s2i_build_as_df APP_PATH SRC_IMAGE DST_IMAGE [S2I_ARGS]
# ----------------------------
# Create a new s2i app image from local sources in a similar way as source-to-image would have used.
# Argument: APP_PATH - local path to the app sources to be used in the test
# Argument: SRC_IMAGE - image to be used as a base for the s2i build
# Argument: DST_IMAGE - image name to be used during the tagging of the s2i build result
# Argument: S2I_ARGS - Additional list of source-to-image arguments.
# Only used to check for pull-policy=never and environment variable definitions.
ct_s2i_build_as_df()
{
local app_path=$1; shift
local src_image=$1; shift
local dst_image=$1; shift
local s2i_args="$*";
local local_app=upload/src/
local local_scripts=upload/scripts/
local user_id=
local df_name=
local tmpdir=
# Use /tmp to not pollute cwd
tmpdir=$(mktemp -d)
df_name=$(mktemp -p "$tmpdir" Dockerfile.XXXX)
pushd "$tmpdir"
# Check if the image is available locally and try to pull it if it is not
docker images "$src_image" &>/dev/null || echo "$s2i_args" | grep -q "pull-policy=never" || docker pull "$src_image"
user_id=$(docker inspect -f "{{.ContainerConfig.User}}" "$src_image")
# Strip file:// from APP_PATH and copy its contents into current context
mkdir -p "$local_app"
cp -r "${app_path/file:\/\//}/." "$local_app"
[ -d "$local_app/.s2i/bin/" ] && mv "$local_app/.s2i/bin" "$local_scripts"
# Create a Dockerfile named df_name and fill it with proper content
#FIXME: Some commands could be combined into a single layer but not sure if worth the trouble for testing purposes
cat <<EOF >"$df_name"
FROM $src_image
LABEL "io.openshift.s2i.build.image"="$src_image" \\
"io.openshift.s2i.build.source-location"="$app_path"
USER root
COPY $local_app /tmp/src
EOF
[ -d "$local_scripts" ] && echo "COPY $local_scripts /tmp/scripts" >> "$df_name" &&
echo "RUN chown -R $user_id:0 /tmp/scripts" >>"$df_name"
echo "RUN chown -R $user_id:0 /tmp/src" >>"$df_name"
# Check for custom environment variables inside .s2i/ folder
if [ -e "$local_app/.s2i/environment" ]; then
# Remove any comments and add the contents as ENV commands to the Dockerfile
sed '/^\s*#.*$/d' "$local_app/.s2i/environment" | while read -r line; do
echo "ENV $line" >>"$df_name"
done
fi
# Filter out env var definitions from $s2i_args and create Dockerfile ENV commands out of them
echo "$s2i_args" | grep -o -e '\(-e\|--env\)[[:space:]=]\S*=\S*' | sed -e 's/-e /ENV /' -e 's/--env[ =]/ENV /' >>"$df_name"
echo "USER $user_id" >>"$df_name"
# If exists, run the custom assemble script, else default to /usr/libexec/s2i/assemble
if [ -x "$local_scripts/assemble" ]; then
echo "RUN /tmp/scripts/assemble" >>"$df_name"
else
echo "RUN /usr/libexec/s2i/assemble" >>"$df_name"
fi
# If exists, set the custom run script as CMD, else default to /usr/libexec/s2i/run
if [ -x "$local_scripts/run" ]; then
echo "CMD /tmp/scripts/run" >>"$df_name"
else
echo "CMD /usr/libexec/s2i/run" >>"$df_name"
fi
# Run the build and tag the result
docker build -f "$df_name" -t "$dst_image" .
popd
}

View file

@ -1,29 +0,0 @@
#!/bin/bash
# First test virtualenv environment and pip upgrade
echo "Testing that the virtual environment's Python is being used ..."
if [ "$(which python)" != "/opt/app-root/bin/python" ]; then
echo "ERROR: Initialization of the virtual environment failed."
exit 1
fi
echo "Testing UPGRADE_PIP_TO_LATEST=1 (set in .s2i/environment) ..."
pip_major_version=$(pip --version | cut -d" " -f2 | cut -d"." -f1)
if [ -z "$pip_major_version" ] || [ "$pip_major_version" -lt "9" ]; then
echo "ERROR: Failed to upgrade pip to version 9 or later."
exit 1
fi
# Now test the uwsgi server
exec uwsgi \
--http-socket :8080 \
--die-on-term \
--master \
--single-interpreter \
--enable-threads \
--threads=5 \
--thunder-lock \
--module wsgi