diff --git a/3.9 b/3.9 new file mode 120000 index 0000000..945c9b4 --- /dev/null +++ b/3.9 @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 16dc51f..9b7dcd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,10 @@ -# This image provides a Python 3.5 environment you can use to run your Python +# This image provides a Python 3.9 environment you can use to run your Python # applications. -FROM registry.fedoraproject.org/f30/s2i-base:latest +FROM registry.fedoraproject.org/f34/s2i-base:latest EXPOSE 8080 -ENV PYTHON_VERSION=3.6 \ +ENV PYTHON_VERSION=3.9 \ PATH=$HOME/.local/bin/:$PATH \ PYTHONUNBUFFERED=1 \ PYTHONIOENCODING=UTF-8 \ @@ -12,12 +12,13 @@ ENV PYTHON_VERSION=3.6 \ LANG=en_US.UTF-8 \ PIP_NO_CACHE_DIR=off + ENV NAME=python3 \ VERSION=0 \ 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. \ @@ -28,37 +29,60 @@ on most platforms." LABEL summary="$SUMMARY" \ description="$DESCRIPTION" \ io.k8s.description="$DESCRIPTION" \ - io.k8s.display-name="Python 3.6" \ + io.k8s.display-name="Python 3.9" \ io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python36,rh-python36" \ + io.openshift.tags="builder,python,python39,python-39,rh-python39" \ com.redhat.component="$NAME" \ name="$FGC/$NAME" \ version="$VERSION" \ - 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.9/test/setup-test-app/ $FGC/$NAME python-sample-app" \ maintainer="SoftwareCollections.org " -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 && \ +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapper \ + httpd httpd-devel atlas-devel gcc-gfortran libffi-devel \ + libtool-ltdl enchant redhat-rpm-config" && \ + 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 +COPY 3.9/s2i/bin/ $STI_SCRIPTS_PATH # Copy extra files to the image. -COPY ./root/ / +COPY 3.9/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-$PYTHON_VERSION /opt/app-root +# Python 3 only +# Yes, the directory below is already copied by the previous command. +# The problem here is that the wheels directory is copied as a symlink. +# Only if you specify symlink directly as a source, COPY copies all the +# files from the symlink destination. +COPY 3.9/root/opt/wheels /opt/wheels +# - Create a Python virtual environment for use by any application to avoid +# potential conflicts with Python packages preinstalled in the main Python +# installation. +# - 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 python3.9 -m venv ${APP_ROOT} && \ +# Python 3 only code, Python 2 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because: \ +# * pip < 9 does not support different packages' versions for Python 2/3 \ +# * pip < 19.3 does not support manylinux2014 wheels. Only manylinux2014 (and later) wheels \ +# support platforms like ppc64le, aarch64 or armv7 \ +# We are newly using wheel from one of the latest stable Fedora releases (from RPM python-pip-wheel) \ +# because it's tested better then whatever version from PyPI and contains useful patches. \ +# We have to do it here (in the macro) so the permissions are correctly fixed and pip is able \ +# to reinstall itself in the next build phases in the assemble script if user wants the latest version \ +${APP_ROOT}/bin/pip install /opt/wheels/pip-* && \ +rm -r /opt/wheels && \ +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 diff --git a/Dockerfile.fedora b/Dockerfile.fedora new file mode 120000 index 0000000..1d1fe94 --- /dev/null +++ b/Dockerfile.fedora @@ -0,0 +1 @@ +Dockerfile \ No newline at end of file diff --git a/README.md b/README.md index a7604ec..31c9cc5 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,159 @@ -Python Docker image -=================== +Python 3.9 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.9 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.9 applications. Users can choose between RHEL and CentOS based builder images. -The resulting image can be run using [Docker](http://docker.io). +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +the CentOS images are available on [Quay.io](https://quay.io/organization/centos7), +and the Fedora images are available in [Fedora Registry](https://registry.fedoraproject.org/). +The resulting image can be run using [podman](https://github.com/containers/libpod) or +[docker](http://docker.io). +Note: while the examples in this README are calling `podman`, you can replace any such calls by `docker` with the same arguments -Usage ---------------------- -To build a simple [python-sample-app](https://github.com/openshift/s2i-python/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: +Description +----------- + +Python 3.9 available as container is a base platform for +building and running various Python 3.9 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 +(see [base image repository](https://github.com/sclorg/s2i-base-container/tree/master/base)), +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 in Openshift +------------------ + +For this, we will assume that you are using the supported image, available via `python:3.9` imagestream tag in Openshift. +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: -* **For RHEL based image** ``` - $ 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 - ``` - -* **For CentOS based image** - ``` - $ 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 + oc new-app python:3.9~https://github.com/sclorg/django-ex.git ``` **Accessing the application:** ``` -$ curl 127.0.0.1:8080 +$ oc get pods +$ oc exec -- curl 127.0.0.1:8080 ``` +Source-to-Image framework and scripts +------------------------------------- +This image supports the [Source-to-Image](https://docs.openshift.com/container-platform/3.11/creating_images/s2i.html) +(S2I) strategy in OpenShift. The Source-to-Image is an OpenShift framework +which makes it easy to write images that take application source code as +an input, use a builder image like this Python container image, and produce +a new image that runs the assembled application as an output. -Repository organization ------------------------- -* **``** +To support the Source-to-Image framework, important scripts are included in the builder image: - * **Dockerfile** +* The `/usr/libexec/s2i/assemble` script inside the image is run to produce a new image with the application artifacts. +The script takes sources of a given application and places them into appropriate directories inside the image. +It utilizes some common patterns in Perl application development (see the **Environment variables** section below). +* The `/usr/libexec/s2i/run` script is set as the default command in the resulting container image (the new image with the application artifacts). +It runs your application according to settings in `APP_MODULE`, `APP_FILE` or `APP_SCRIPT` environment variables or it tries to detect the best +way automatically. - CentOS based Dockerfile. +Building an application using a Dockerfile +------------------------------------------ +Compared to the Source-to-Image strategy, using a Dockerfile is a more +flexible way to build a Python container image with an application. +Use a Dockerfile when Source-to-Image is not sufficiently flexible for you or +when you build the image outside of the OpenShift environment. - * **Dockerfile.rhel7** +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on - 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. +``` +podman pull registry.access.redhat.com/ubi8/python-39 +``` - * **`s2i/bin/`** +#### 2. Pull and application code - This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): +An example application available at https://github.com/sclorg/django-ex.git is used here. Feel free to clone the repository for further experiments. +You can also take a look at code examples in s2i-python-container repository: https://github.com/sclorg/s2i-python-container/tree/master/examples - * **assemble** +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` - Used to install the sources into the location where the application - will be run and prepare the application for deployment (eg. installing - dependencies, etc.) +#### 3. Prepare an application inside a container - * **run** +This step usually consists of at least these parts: - This script is responsible for running the application by using the - application web server. +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image - * **usage*** +For all these three parts, users can either setup all manually and use commands `python` and `pip` explicitly in the Dockerfile, +or users can use the Source-to-Image scripts inside the image. - This script prints the usage of this image. +The manual way comes with the highest level of flexibility but requires you to know how to work +with modules or software collections manually, how to setup virtual environment with the right version +of Python and many more. On the other hand, using Source-to-Image scripts makes your Dockerfile +prepared for a future flawless switch to a newer or different platform. - * **`contrib/`** +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: - This folder contains a file with commonly used modules. +``` +FROM registry.access.redhat.com/ubi8/python-39 - * **`test/`** +# Add application sources to a directory that the assemble script expects them +# and set permissions so that the container runs without root access +USER 0 +ADD app-src /tmp/src +RUN /usr/bin/fix-permissions /tmp/src +USER 1001 - This folder contains a [S2I](https://github.com/openshift/source-to-image) - test framework with a simple server. +# Install the dependencies +RUN /usr/libexec/s2i/assemble - * **`setup-test-app/`** +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` - Simple Gunicorn application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. +If you decide not to use the Source-to-Image scripts, you will need to manually tailor the Dockerfile to your application and its needs. +Example Dockerfile for a simple Django application: - * **`standalone-test-app/`** +``` +FROM registry.access.redhat.com/ubi8/python-39 - Simple standalone application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. +# Add application sources with correct permissions for OpenShift +USER 0 +ADD app-src . +RUN chown -R 1001:0 ./ +USER 1001 - * **run** +# Install the dependencies +RUN pip install -U "pip>=19.3.1" && \ + pip install -r requirements.txt && \ + python manage.py collectstatic --noinput && \ + python manage.py migrate - Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. +# Run the application +CMD python manage.py runserver 0.0.0.0:8080 +``` +#### 4. Build a new image from a Dockerfile prepared in the previous step + +``` +podman build -t python-app . +``` + +#### 5. Run the resulting image with final application + +``` +podman run -d python-app +``` Environment variables --------------------- @@ -119,7 +188,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.9/test/setup-test-app). * **APP_HOME** @@ -134,33 +203,83 @@ 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,cfg} 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/pypa/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. +* **PIN_PIPENV_VERSION** + + Set this variable together with `ENABLE_PIPENV` to use a specific version of Pipenv. + If not set, the latest stable version from PyPI is installed. + For example `PIN_PIPENV_VERSION=2018.11.26` installs `pipenv==2018.11.26`. + +* **ENABLE_MICROPIPENV** + + Set this variable to use [micropipenv](https://github.com/thoth-station/micropipenv), + a lightweight wrapper for pip to support requirements.txt, Pipenv and Poetry lock + files or converting them to pip-tools compatible output. Designed for containerized Python applications. + Available only for Python 3 images. + +* **ENABLE_INIT_WRAPPER** + + Set this variable to a non-empty value to make use of an init wrapper. + This is useful for servers that are not capable of reaping zombie + processes, such as Django development server or Tornado. This option can + be used together with **APP_SCRIPT** or **APP_FILE**. It never applies + to Gunicorn used through **APP_MODULE** as Gunicorn reaps zombie + processes correctly. * **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. + Set this variable to use a custom index URL or mirror to download required + packages during build process. This affects packages listed in + requirements.txt. It also affects the installation of pipenv and + micropipenv and the update of pip in the container, though if not found in + the custom index, the container will try to install/update them from + upstream PyPI afterwards. + +* **PORT** + + HTTP(S) port your application should listen on. The default is 8080. + `PORT` is used only for Django development server and for Gunicorn + with the default configutation (no `APP_CONFIG` or `GUNICORN_CMD_ARGS` specified). * **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, the container will use + the stable pip version this container was built with, taken from a recent Fedora release. * **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 +293,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`. - + [here](https://packaging.python.org/guides/distributing-packages-using-setuptools/?highlight=distributing#setup-py). + 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** @@ -202,6 +329,9 @@ following ways, in precedence order: If you have both Django and Gunicorn in your requirements, your Django project will automatically be served using Gunicorn. + The default setting for Gunicorn (`--bind=0.0.0.0:$PORT --access-logfile=-`) is applied + only if both `$APP_CONFIG` and `$GUNICORN_CMD_ARGS` are not defined. + * **Django development server** If you have Django in your requirements but don't have Gunicorn, then your @@ -225,7 +355,7 @@ following ways, in precedence order: application. Hot deploy ---------------------- +---------- If you are using Django, hot deploy will work out of the box. @@ -235,12 +365,20 @@ configuration file inside your repository with the 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: +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: ``` -docker exec -it /bin/bash +podman 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. + + +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 RHEL7 is called `Dockerfile.rhel7`, +for RHEL8 it's `Dockerfile.rhel8` and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/bot-cfg.yml b/bot-cfg.yml new file mode 100644 index 0000000..50767fd --- /dev/null +++ b/bot-cfg.yml @@ -0,0 +1,23 @@ +version: "1" +betka: + # is betka enabled for this repository + # optional - defaults to false + enabled: true + # optional + notifications: + email_addresses: [phracek@redhat.com, lbalhar@redhat.com] + # Specify if master branch in upstream repository is synced + master_checker: true + # Should pull requests be synced? + # optional + pr_checker: false + # Either 'upstream_branch_name' or 'upstream_git_path' has to be specified + # Branch name which is used for sync + upstream_branch_name: master + # Path to directory with dockerfile withing upstream repository + upstream_git_path: "3.8" + # Github comment message to enforce sync of a pull request + # required if pr_checker is true otherwise optional + pr_comment_message: "[test]" + # optional + image_url: quay.io/rhscl/cwt-generator diff --git a/help.md b/help.md new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/help.md @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/root/opt/app-root/etc/scl_enable b/root/opt/app-root/etc/scl_enable index 661da3c..766e4de 100644 --- a/root/opt/app-root/etc/scl_enable +++ b/root/opt/app-root/etc/scl_enable @@ -2,5 +2,8 @@ # 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 +if head "/etc/redhat-release" | grep -q "^CentOS Linux release 7" || \ + head "/etc/redhat-release" | grep -q "^Red Hat Enterprise Linux\( Server\)\? release 7"; then + source scl_source enable httpd24 $NODEJS_SCL rh-python39 +fi source /opt/app-root/bin/activate diff --git a/root/opt/wheels/pip-21.0.1-py3-none-any.whl b/root/opt/wheels/pip-21.0.1-py3-none-any.whl new file mode 100755 index 0000000..87a37e6 Binary files /dev/null and b/root/opt/wheels/pip-21.0.1-py3-none-any.whl differ diff --git a/root/opt/wheels/pip-21.2.3-py3-none-any.whl b/root/opt/wheels/pip-21.2.3-py3-none-any.whl new file mode 100755 index 0000000..a337a36 Binary files /dev/null and b/root/opt/wheels/pip-21.2.3-py3-none-any.whl differ diff --git a/s2i/bin/assemble b/s2i/bin/assemble index 846bdbf..18c8373 100755 --- a/s2i/bin/assemble +++ b/s2i/bin/assemble @@ -8,35 +8,99 @@ function should_collectstatic() { is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] } +function virtualenv_bin() { + # New versions of Python (>3.6) should use venv module + # from stdlib instead of virtualenv package + python3.9 -m venv $1 +} + +# Install pipenv or micropipenv 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_tool() { + echo "---> Installing $1 packaging tool ..." + VENV_DIR=$HOME/.local/venvs/$1 + virtualenv_bin "$VENV_DIR" + # First, try to install the tool without --isolated which means that if you + # have your own PyPI mirror, it will take it from there. If this try fails, try it + # again with --isolated which ignores external pip settings (env vars, config file) + # and installs the tool from PyPI (needs internet connetion). + # $1$2 combines package name with [extras] or version specifier if is defined as $2``` + if ! $VENV_DIR/bin/pip install -U $1$2; then + echo "WARNING: Installation of $1 failed, trying again from official PyPI with pip --isolated install" + $VENV_DIR/bin/pip install --isolated -U $1$2 # Combines package name with [extras] or version specifier if is defined as $2``` + fi + mkdir -p $HOME/.local/bin + ln -s $VENV_DIR/bin/$1 $HOME/.local/bin/$1 +} + set -e +# First of all, check that we don't have disallowed combination of ENVs +if [[ ! -z "$ENABLE_PIPENV" && ! -z "$ENABLE_MICROPIPENV" ]]; then + echo "ERROR: Pipenv and micropipenv cannot be enabled at the same time!" + # podman/buildah does not relay this exit code but it will be fixed hopefuly + # https://github.com/containers/buildah/issues/2305 + exit 3 +fi + shopt -s dotglob echo "---> Installing application source ..." -mv /tmp/src/* ./ +mv /tmp/src/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then - echo "---> Upgrading pip to latest version ..." - pip install -U pip + echo "---> Upgrading pip, setuptools and wheel to latest version ..." + if ! pip install -U pip setuptools wheel; then + echo "WARNING: Installation of the latest pip, setuptools and wheel failed, trying again from official PyPI with pip --isolated install" + pip install --isolated -U pip setuptools wheel + fi fi -if [[ -f requirements.txt ]]; then +if [[ ! -z "$ENABLE_PIPENV" ]]; then + if [[ ! -z "$PIN_PIPENV_VERSION" ]]; then + # Add == as a prefix to pipenv version, if defined + PIN_PIPENV_VERSION="==$PIN_PIPENV_VERSION" + fi + install_tool "pipenv" "$PIN_PIPENV_VERSION" + 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 [[ ! -z "$ENABLE_MICROPIPENV" ]]; then + install_tool "micropipenv" "[toml]" + echo "---> Installing dependencies via micropipenv ..." + # micropipenv detects Pipfile.lock and requirements.txt in this order + micropipenv install --deploy +elif [[ -f requirements.txt ]]; then echo "---> Installing dependencies ..." pip install -r requirements.txt fi -if [[ -f setup.py ]]; then +if [[ ( -f setup.py || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then echo "---> Installing application ..." - python setup.py develop + pip install . fi if should_collectstatic; then ( echo "---> Collecting Django static files ..." + APP_HOME=$(readlink -f "${APP_HOME:-.}") + # Change the working directory to APP_HOME + PYTHONPATH="$(pwd)${PYTHONPATH:+:$PYTHONPATH}" + cd "$APP_HOME" - 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 + # Look for 'manage.py' in the current directory + manage_file=./manage.py if [[ ! -f "$manage_file" ]]; then echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." @@ -56,4 +120,4 @@ if should_collectstatic; then fi # set permissions for any installed artifacts -fix-permissions /opt/app-root +fix-permissions /opt/app-root -P diff --git a/s2i/bin/init-wrapper b/s2i/bin/init-wrapper new file mode 100755 index 0000000..26c8767 --- /dev/null +++ b/s2i/bin/init-wrapper @@ -0,0 +1,18 @@ +#!/bin/sh + +# Overview of how this script works: http://veithen.io/2014/11/16/sigterm-propagation.html +# Set a trap to kill the main app process when this +# init script receives SIGTERM or SIGINT +trap 'kill -s TERM $PID' TERM INT +# Execute the main application in the background +"$@" & +PID=$! +# wait command always terminates when trap is caught, even if the process hasn't finished yet +wait $PID +# Remove the trap and wait till the app process finishes completely +trap - TERM INT +# We wait again, since the first wait terminates when trap is caught +wait $PID +# Exit with the exit code of the app process +STATUS=$? +exit $STATUS diff --git a/s2i/bin/run b/s2i/bin/run index 7037205..21a38f4 100755 --- a/s2i/bin/run +++ b/s2i/bin/run @@ -29,33 +29,59 @@ 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 } -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" +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" fi - exec "$APP_SCRIPT" -else - test -n "$app_script_check" && (>&2 echo "ERROR: file '$app_script_check' not found.") && exit 1 +} + +APP_HOME=$(readlink -f "${APP_HOME:-.}") +# Change the working directory to APP_HOME +PYTHONPATH="$(pwd)${PYTHONPATH:+:$PYTHONPATH}" +cd "$APP_HOME" + +if [ -z "$APP_SCRIPT" ] && [ -z "$APP_FILE" ] && [ -z "$APP_MODULE" ]; then + # Set default values for APP_SCRIPT and APP_FILE only when all three APP_ + # variables are not defined by user. This prevents a situation when + # APP_MODULE is defined to app:application but the app.py file is found as the + # APP_FILE and then executed by Python instead of gunicorn. + APP_SCRIPT="app.sh" + APP_SCRIPT_DEFAULT=1 + APP_FILE="app.py" + APP_FILE_DEFAULT=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 +if [ ! -z "$APP_SCRIPT" ]; then + if [[ -f "$APP_SCRIPT" ]]; then + echo "---> Running application from script ($APP_SCRIPT) ..." + if [[ "$APP_SCRIPT" != /* ]]; then + APP_SCRIPT="./$APP_SCRIPT" + fi + maybe_run_in_init_wrapper "$APP_SCRIPT" + elif [[ -z "$APP_SCRIPT_DEFAULT" ]]; then + echo "ERROR: file '$APP_SCRIPT' not found." && exit 1 + fi 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 [ ! -z "$APP_FILE" ]; then + if [[ -f "$APP_FILE" ]]; then + echo "---> Running application from Python script ($APP_FILE) ..." + maybe_run_in_init_wrapper python "$APP_FILE" + elif [[ -z "$APP_FILE_DEFAULT" ]]; then + echo "ERROR: file '$APP_FILE' not found." && exit 1 + fi +fi + +# Look for 'manage.py' in the current directory +manage_file=./manage.py if should_migrate; then if [[ -f "$manage_file" ]]; then @@ -67,31 +93,44 @@ if should_migrate; then 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 not set, use 8080 as the default port +if [ -z "$PORT" ]; then + PORT=8080 +fi - if [[ -z "$APP_MODULE" && -f setup.py ]]; then - APP_MODULE="$(python setup.py --name)" +if is_gunicorn_installed; then + setup_py=$(find "$HOME" -maxdepth 2 -type f -name 'setup.py' -print -quit) + # Look for wsgi module in the current directory + if [[ -z "$APP_MODULE" && -f "./wsgi.py" ]]; then + APP_MODULE=wsgi + elif [[ -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" + # Default settings for gunicorn if none of the custom are set + if [ -z "$APP_CONFIG" ] && [ -z "$GUNICORN_CMD_ARGS" ]; then + GUNICORN_CMD_ARGS="--bind=0.0.0.0:$PORT --access-logfile=-" + gunicorn_settings_source="default" + else + gunicorn_settings_source="custom" + fi + + # Gunicorn can read GUNICORN_CMD_ARGS as an env variable but because this is not + # supported in Gunicorn < 20 we still need for Python 2, we are using arguments directly. + echo "---> Serving application with gunicorn ($APP_MODULE) with $gunicorn_settings_source settings ..." + exec gunicorn "$APP_MODULE" $GUNICORN_CMD_ARGS --config "$APP_CONFIG" fi fi if is_django_installed; then if [[ -f "$manage_file" ]]; then - echo "---> Serving application with 'manage.py runserver' ..." + echo "---> Serving application with 'manage.py runserver 0.0.0.0:$PORT' ..." 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 + maybe_run_in_init_wrapper python "$manage_file" runserver 0.0.0.0:$PORT else echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." echo "Skipped 'python manage.py runserver'." diff --git a/s2i/bin/usage b/s2i/bin/usage index b1b09f0..a45027f 100755 --- a/s2i/bin/usage +++ b/s2i/bin/usage @@ -5,14 +5,14 @@ NAMESPACE=centos [[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl cat < -- curl 127.0.0.1:8080 EOF diff --git a/test/app-home-test-app/requirements.txt b/test/app-home-test-app/requirements.txt index 8f22dcc..268bd60 100644 --- a/test/app-home-test-app/requirements.txt +++ b/test/app-home-test-app/requirements.txt @@ -1 +1,2 @@ -gunicorn +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/app-module-test-app/.gitignore b/test/app-module-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/app-module-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/app-module-test-app/.s2i/environment b/test/app-module-test-app/.s2i/environment new file mode 100644 index 0000000..ac798d7 --- /dev/null +++ b/test/app-module-test-app/.s2i/environment @@ -0,0 +1 @@ +APP_MODULE=app:application diff --git a/test/app-module-test-app/app.py b/test/app-module-test-app/app.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/app-module-test-app/app.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/app-module-test-app/requirements.txt b/test/app-module-test-app/requirements.txt new file mode 100644 index 0000000..268bd60 --- /dev/null +++ b/test/app-module-test-app/requirements.txt @@ -0,0 +1,2 @@ +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/check_imagestreams.py b/test/check_imagestreams.py new file mode 100755 index 0000000..d759277 --- /dev/null +++ b/test/check_imagestreams.py @@ -0,0 +1,101 @@ +#!/bin/env python3 + +# MIT License +# +# Copyright (c) 2018-2019 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import sys +import json +import logging +import os + +from pathlib import Path +from typing import Dict, List, Any + +IMAGESTREAMS_DIR: str = "imagestreams" + + +class ImageStreamChecker(object): + version: str = "" + + def __init__(self, version: str): + self.version = version + self.results: Dict[Any, Any] = {} + + def load_json_file(self, filename: Path) -> Any: + with open(str(filename)) as f: + data = json.load(f) + isinstance(data, Dict) + return data + + def check_version(self, json_dict: Dict[Any, Any]) -> List[str]: + res = [] + for tags in json_dict["spec"]["tags"]: + # The name can be"" or "-elX" or "-ubiX" + if tags["name"] == self.version or tags["name"].startswith( + self.version + "-" + ): + res.append(tags) + return res + + def check_latest_tag(self, json_dict: Dict[Any, Any]) -> bool: + latest_tag_correct: bool = False + for tags in json_dict["spec"]["tags"]: + if tags["name"] != "latest": + continue + # The latest can link to either "" or "-elX" or "-ubiX" + if tags["from"]["name"] == self.version or tags["from"]["name"].startswith( + self.version + "-" + ): + latest_tag_correct = True + return latest_tag_correct + + def check_imagestreams(self) -> int: + p = Path(".") + json_files = p.glob(f"{IMAGESTREAMS_DIR}/*.json") + if not json_files: + print(f"No json files present in {IMAGESTREAMS_DIR}.") + return 0 + for f in json_files: + if os.environ.get("TARGET") in ("rhel7", "centos7") and "aarch64" in str(f): + print("Imagestream aarch64 is not supported on rhel7") + continue + print(f"Checking file {str(f)}.") + json_dict = self.load_json_file(f) + if not (self.check_version(json_dict) and self.check_latest_tag(json_dict)): + print( + f"The latest version is not present in {str(f)} or in latest tag." + ) + self.results[f] = False + if self.results: + return 1 + print("Imagestreams contains the latest version.") + return 0 + + +if __name__ == "__main__": + if len(sys.argv) != 2: + logging.fatal("%s: %s", sys.argv[0], "VERSION as an argument was not provided") + sys.exit(1) + + print(f"Version to check is {sys.argv[1]}.") + isc = ImageStreamChecker(version=sys.argv[1]) + sys.exit(isc.check_imagestreams()) diff --git a/test/django-different-port-test-app/.gitignore b/test/django-different-port-test-app/.gitignore new file mode 100644 index 0000000..651f49f --- /dev/null +++ b/test/django-different-port-test-app/.gitignore @@ -0,0 +1,61 @@ +# Django +db.sqlite3 +staticfiles/ + +# 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/django-different-port-test-app/.s2i/environment b/test/django-different-port-test-app/.s2i/environment new file mode 100644 index 0000000..990affa --- /dev/null +++ b/test/django-different-port-test-app/.s2i/environment @@ -0,0 +1 @@ +PORT=8085 diff --git a/test/django-different-port-test-app/manage.py b/test/django-different-port-test-app/manage.py new file mode 100755 index 0000000..82cfa83 --- /dev/null +++ b/test/django-different-port-test-app/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/test/app-home-test-app/project/__init__.py b/test/django-different-port-test-app/project/__init__.py similarity index 100% rename from test/app-home-test-app/project/__init__.py rename to test/django-different-port-test-app/project/__init__.py diff --git a/test/django-different-port-test-app/project/settings.py b/test/django-different-port-test-app/project/settings.py new file mode 100644 index 0000000..79072d0 --- /dev/null +++ b/test/django-different-port-test-app/project/settings.py @@ -0,0 +1,118 @@ +""" +Django settings for project project. + +Generated by 'django-admin startproject' using Django 1.8.1. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.8/ref/settings/ +""" + +import django +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'y*b^6p#z&cm2)8rzgbp2i4k*+rg2h%60l*bmf6hg&ro!z0-ael' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# SECURITY WARNING: do not use '*' on production or use some HTTP(S) proxy +ALLOWED_HOSTS = ['*'] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +) + +# Django 1 +if django.VERSION[0] == 1: + MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + ) +else: + # Django 2+ + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + ) + +ROOT_URLCONF = 'project.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'project.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.8/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.8/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles') diff --git a/test/django-different-port-test-app/project/urls.py b/test/django-different-port-test-app/project/urls.py new file mode 100644 index 0000000..dde7636 --- /dev/null +++ b/test/django-different-port-test-app/project/urls.py @@ -0,0 +1,21 @@ +"""project URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.8/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Add an import: from blog import urls as blog_urls + 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) +""" +from django.conf.urls import include, url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/test/django-different-port-test-app/project/wsgi.py b/test/django-different-port-test-app/project/wsgi.py new file mode 100644 index 0000000..7c2f625 --- /dev/null +++ b/test/django-different-port-test-app/project/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for project project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") + +application = get_wsgi_application() diff --git a/test/django-different-port-test-app/requirements.txt b/test/django-different-port-test-app/requirements.txt new file mode 100644 index 0000000..901c5f8 --- /dev/null +++ b/test/django-different-port-test-app/requirements.txt @@ -0,0 +1,2 @@ +Django<3; python_version > '3.8' +Django<2; python_version <='3.8' diff --git a/test/django-test-app/project/settings.py b/test/django-test-app/project/settings.py index 3b5224b..79072d0 100644 --- a/test/django-test-app/project/settings.py +++ b/test/django-test-app/project/settings.py @@ -10,6 +10,7 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.8/ref/settings/ """ +import django # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os @@ -25,7 +26,8 @@ SECRET_KEY = 'y*b^6p#z&cm2)8rzgbp2i4k*+rg2h%60l*bmf6hg&ro!z0-ael' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [] +# SECURITY WARNING: do not use '*' on production or use some HTTP(S) proxy +ALLOWED_HOSTS = ['*'] # Application definition @@ -39,16 +41,29 @@ INSTALLED_APPS = ( 'django.contrib.staticfiles', ) -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', -) +# Django 1 +if django.VERSION[0] == 1: + MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + ) +else: + # Django 2+ + MIDDLEWARE = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'django.middleware.security.SecurityMiddleware', + ) ROOT_URLCONF = 'project.urls' diff --git a/test/django-test-app/project/urls.py b/test/django-test-app/project/urls.py index b5e472c..dde7636 100644 --- a/test/django-test-app/project/urls.py +++ b/test/django-test-app/project/urls.py @@ -17,5 +17,5 @@ from django.conf.urls import include, url from django.contrib import admin urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), ] diff --git a/test/django-test-app/requirements.txt b/test/django-test-app/requirements.txt index 18f5860..901c5f8 100644 --- a/test/django-test-app/requirements.txt +++ b/test/django-test-app/requirements.txt @@ -1 +1,2 @@ -Django==1.8.1 +Django<3; python_version > '3.8' +Django<2; python_version <='3.8' diff --git a/test/from-dockerfile/Dockerfile.tpl b/test/from-dockerfile/Dockerfile.tpl new file mode 100644 index 0000000..4249e1e --- /dev/null +++ b/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1,14 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +# Add application sources to a directory that the assemble script expects them +# and set permissions so that the container runs without root access +USER 0 +ADD app-src /tmp/src +RUN /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run diff --git a/test/from-dockerfile/Dockerfile_no_s2i.tpl b/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 0000000..da2f43f --- /dev/null +++ b/test/from-dockerfile/Dockerfile_no_s2i.tpl @@ -0,0 +1,16 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +# Add application sources with correct permissions for OpenShift +USER 0 +ADD app-src . +RUN chown -R 1001:0 ./ +USER 1001 + +# Install the dependencies +RUN pip install -U "pip>=19.3.1" && \ + pip install -r requirements.txt && \ + python manage.py collectstatic --noinput && \ + python manage.py migrate + +# Run the application +CMD python manage.py runserver 0.0.0.0:8080 diff --git a/test/gunicorn-config-different-port-test-app/.gitignore b/test/gunicorn-config-different-port-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/gunicorn-config-different-port-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/gunicorn-config-different-port-test-app/.s2i/environment b/test/gunicorn-config-different-port-test-app/.s2i/environment new file mode 100644 index 0000000..d975f74 --- /dev/null +++ b/test/gunicorn-config-different-port-test-app/.s2i/environment @@ -0,0 +1,2 @@ +APP_MODULE=app:application +APP_CONFIG=./gunicorn.conf.py diff --git a/test/gunicorn-config-different-port-test-app/app.py b/test/gunicorn-config-different-port-test-app/app.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/gunicorn-config-different-port-test-app/app.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/gunicorn-config-different-port-test-app/gunicorn.conf.py b/test/gunicorn-config-different-port-test-app/gunicorn.conf.py new file mode 100644 index 0000000..1ae0446 --- /dev/null +++ b/test/gunicorn-config-different-port-test-app/gunicorn.conf.py @@ -0,0 +1,4 @@ +import multiprocessing + +bind = "0.0.0.0:8085" +workers = multiprocessing.cpu_count() * 2 + 1 diff --git a/test/gunicorn-config-different-port-test-app/requirements.txt b/test/gunicorn-config-different-port-test-app/requirements.txt new file mode 100644 index 0000000..268bd60 --- /dev/null +++ b/test/gunicorn-config-different-port-test-app/requirements.txt @@ -0,0 +1,2 @@ +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/gunicorn-different-port-test-app/.gitignore b/test/gunicorn-different-port-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/gunicorn-different-port-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/gunicorn-different-port-test-app/.s2i/environment b/test/gunicorn-different-port-test-app/.s2i/environment new file mode 100644 index 0000000..164666b --- /dev/null +++ b/test/gunicorn-different-port-test-app/.s2i/environment @@ -0,0 +1,2 @@ +APP_MODULE=app:application +PORT=8085 diff --git a/test/gunicorn-different-port-test-app/app.py b/test/gunicorn-different-port-test-app/app.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/gunicorn-different-port-test-app/app.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/gunicorn-different-port-test-app/requirements.txt b/test/gunicorn-different-port-test-app/requirements.txt new file mode 100644 index 0000000..268bd60 --- /dev/null +++ b/test/gunicorn-different-port-test-app/requirements.txt @@ -0,0 +1,2 @@ +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/imagestreams/python-centos.json b/test/imagestreams/python-centos.json new file mode 100644 index 0000000..405c06e --- /dev/null +++ b/test/imagestreams/python-centos.json @@ -0,0 +1,173 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python applications on UBI. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version of Python available on OpenShift, including major version updates.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.9-ubi8" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.9-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.9,python", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi8/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.8 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi8/python-38:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8-ubi7", + "annotations": { + "openshift.io/display-name": "Python 3.8 (UBI 7)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on UBI 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi7/python-38:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.6-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.6 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.6 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.6/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.6,python", + "version": "3.6", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi8/python-36:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7-ubi8", + "annotations": { + "openshift.io/display-name": "Python 2.7 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi8/python-27:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7-ubi7", + "annotations": { + "openshift.io/display-name": "Python 2.7 (UBI 7)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on UBI 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi7/python-27:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7", + "annotations": { + "openshift.io/display-name": "Python 2.7", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on CentOS 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python,hidden", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "quay.io/centos7/python-27-centos7:latest" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/test/imagestreams/python-centos7.json b/test/imagestreams/python-centos7.json new file mode 100644 index 0000000..bc129c0 --- /dev/null +++ b/test/imagestreams/python-centos7.json @@ -0,0 +1,73 @@ +{ + "kind": "ImageStream", + "apiVersion": "v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python applications on CentOS 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.6/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version of Python available on OpenShift, including major version updates.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.6" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.6", + "annotations": { + "openshift.io/display-name": "Python 3.6", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.6 applications on CentOS 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.6/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.6,python", + "version": "3.6", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "docker.io/centos/python-36-centos7:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7", + "annotations": { + "openshift.io/display-name": "Python 2.7", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on CentOS 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "docker.io/centos/python-27-centos7:latest" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/test/imagestreams/python-rhel-aarch64.json b/test/imagestreams/python-rhel-aarch64.json new file mode 100644 index 0000000..f1400c5 --- /dev/null +++ b/test/imagestreams/python-rhel-aarch64.json @@ -0,0 +1,113 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python applications on UBI. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version of Python available on OpenShift, including major version updates.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.9-ubi8" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.9-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.9,python", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.8 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-38:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.6-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.6 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.6 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.6/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.6,python", + "version": "3.6", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-36:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7-ubi8", + "annotations": { + "openshift.io/display-name": "Python 2.7 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-27:latest" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/test/imagestreams/python-rhel.json b/test/imagestreams/python-rhel.json new file mode 100644 index 0000000..92f545f --- /dev/null +++ b/test/imagestreams/python-rhel.json @@ -0,0 +1,193 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python applications on UBI. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version of Python available on OpenShift, including major version updates.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.9-ubi8" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.9-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.9/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.9,python", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.8 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-38:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8-ubi7", + "annotations": { + "openshift.io/display-name": "Python 3.8 (UBI 7)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on UBI 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi7/python-38:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.8", + "annotations": { + "openshift.io/display-name": "Python 3.8", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.8 applications on RHEL 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.8/README.md.", + "iconClass": "icon-python", + "tags": "builder,python,hidden", + "supports":"python:3.8,python", + "version": "3.8", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/rhscl/python-38-rhel7:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.6-ubi8", + "annotations": { + "openshift.io/display-name": "Python 3.6 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.6 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.6/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:3.6,python", + "version": "3.6", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-36:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7-ubi8", + "annotations": { + "openshift.io/display-name": "Python 2.7 (UBI 8)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on UBI 8. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi8/python-27:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7-ubi7", + "annotations": { + "openshift.io/display-name": "Python 2.7 (UBI 7)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on UBI 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi7/python-27:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "2.7", + "annotations": { + "openshift.io/display-name": "Python 2.7", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 2.7 applications on RHEL 7. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/2.7/README.md.", + "iconClass": "icon-python", + "tags": "builder,python,hidden", + "supports":"python:2.7,python", + "version": "2.7", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/rhscl/python-27-rhel7:latest" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/test/locale-test-app/requirements.txt b/test/locale-test-app/requirements.txt index cef5a16..51bc227 100644 --- a/test/locale-test-app/requirements.txt +++ b/test/locale-test-app/requirements.txt @@ -1,2 +1,3 @@ Flask -gunicorn +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' diff --git a/test/micropipenv-requirements-test-app/.s2i/environment b/test/micropipenv-requirements-test-app/.s2i/environment new file mode 100644 index 0000000..2d40b15 --- /dev/null +++ b/test/micropipenv-requirements-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_MICROPIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/test/micropipenv-requirements-test-app/app.py b/test/micropipenv-requirements-test-app/app.py new file mode 100644 index 0000000..bbac264 --- /dev/null +++ b/test/micropipenv-requirements-test-app/app.py @@ -0,0 +1,14 @@ +import os +import mod_wsgi.server + +mod_wsgi.server.start( + '--log-to-terminal', + '--port', '8080', + '--trust-proxy-header', 'X-Forwarded-For', + '--trust-proxy-header', 'X-Forwarded-Port', + '--trust-proxy-header', 'X-Forwarded-Proto', + '--processes', os.environ.get('MOD_WSGI_PROCESSES', '1'), + '--threads', os.environ.get('MOD_WSGI_THREADS', '5'), + '--application-type', 'module', + '--entry-point', 'wsgi' +) diff --git a/test/micropipenv-requirements-test-app/requirements.txt b/test/micropipenv-requirements-test-app/requirements.txt new file mode 100644 index 0000000..a74adef --- /dev/null +++ b/test/micropipenv-requirements-test-app/requirements.txt @@ -0,0 +1,2 @@ +mod_wsgi +Flask diff --git a/test/micropipenv-requirements-test-app/wsgi.py b/test/micropipenv-requirements-test-app/wsgi.py new file mode 100644 index 0000000..9e72469 --- /dev/null +++ b/test/micropipenv-requirements-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from mod_wsgi hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/test/micropipenv-test-app/.gitignore b/test/micropipenv-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/micropipenv-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/micropipenv-test-app/.s2i/environment b/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 0000000..29efed4 --- /dev/null +++ b/test/micropipenv-test-app/.s2i/environment @@ -0,0 +1,5 @@ +ENABLE_MICROPIPENV=true +DISABLE_SETUP_PY_PROCESSING=true +# This tests second try to install micropipenv with --isolated +# because the first one won't work with following setting +PIP_INDEX_URL=https://example.com/ diff --git a/test/micropipenv-test-app/Pipfile b/test/micropipenv-test-app/Pipfile new file mode 100644 index 0000000..117b223 --- /dev/null +++ b/test/micropipenv-test-app/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.20.0" + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.9" diff --git a/test/micropipenv-test-app/Pipfile.lock b/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 0000000..2574173 --- /dev/null +++ b/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,145 @@ +{ + "_meta": { + "hash": { + "sha256": "9d47e9d2d35583e081cf783e9421e75fafb102d07969ad6f74546f48db3a8101" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "markers": "python_version >= '3.4'", + "version": "==20.0.4" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "requests": { + "hashes": [ + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" + ], + "index": "pypi", + "version": "==2.20.0" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", + "version": "==1.24.3" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", + "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" + ], + "index": "pypi", + "version": "==6.1.1" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + } + } +} diff --git a/test/micropipenv-test-app/setup.py b/test/micropipenv-test-app/setup.py new file mode 100644 index 0000000..e313409 --- /dev/null +++ b/test/micropipenv-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/micropipenv-test-app/testapp.py b/test/micropipenv-test-app/testapp.py new file mode 100644 index 0000000..a9193ee --- /dev/null +++ b/test/micropipenv-test-app/testapp.py @@ -0,0 +1,7 @@ +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/test/virtualenv-uwsgi-test-app/.s2i/environment b/test/npm-virtualenv-uwsgi-test-app/.s2i/environment similarity index 100% rename from test/virtualenv-uwsgi-test-app/.s2i/environment rename to test/npm-virtualenv-uwsgi-test-app/.s2i/environment diff --git a/test/npm-virtualenv-uwsgi-test-app/app.sh b/test/npm-virtualenv-uwsgi-test-app/app.sh new file mode 100755 index 0000000..5b7a273 --- /dev/null +++ b/test/npm-virtualenv-uwsgi-test-app/app.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# 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) ..." +packages=("pip" "setuptools" "wheel") +for pkg in ${packages[@]}; do + # grep returns exit code 1 if the output contains only one line starting + # with "Requirement already …" which means that the package is updated + python -m pip install -U --no-deps --no-python-version-warning $pkg 2>&1 | grep -Ev "^Requirement already (up-to-date|satisfied): " + if [ $? -eq 0 ]; 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 diff --git a/test/virtualenv-uwsgi-test-app/requirements.txt b/test/npm-virtualenv-uwsgi-test-app/requirements.txt similarity index 100% rename from test/virtualenv-uwsgi-test-app/requirements.txt rename to test/npm-virtualenv-uwsgi-test-app/requirements.txt diff --git a/test/virtualenv-uwsgi-test-app/wsgi.py b/test/npm-virtualenv-uwsgi-test-app/wsgi.py similarity index 100% rename from test/virtualenv-uwsgi-test-app/wsgi.py rename to test/npm-virtualenv-uwsgi-test-app/wsgi.py diff --git a/test/numpy-test-app/requirements.txt b/test/numpy-test-app/requirements.txt index 8978a0f..e8dd2c3 100644 --- a/test/numpy-test-app/requirements.txt +++ b/test/numpy-test-app/requirements.txt @@ -1,2 +1,4 @@ -gunicorn -numpy +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' +numpy<1.17.0; python_version < '3.5' +numpy>=1.17.0; python_version >= '3.5' diff --git a/test/pin-pipenv-version-test-app b/test/pin-pipenv-version-test-app new file mode 160000 index 0000000..d805fc9 --- /dev/null +++ b/test/pin-pipenv-version-test-app @@ -0,0 +1 @@ +Subproject commit d805fc9986bc087d2dd6dd6655f96097699c56fb diff --git a/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment b/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment new file mode 100644 index 0000000..e3932ee --- /dev/null +++ b/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +ENABLE_MICROPIPENV=true diff --git a/test/pipenv-test-app/.gitignore b/test/pipenv-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/pipenv-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/pipenv-test-app/.s2i/environment b/test/pipenv-test-app/.s2i/environment new file mode 100644 index 0000000..4a4f760 --- /dev/null +++ b/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/test/pipenv-test-app/Pipfile b/test/pipenv-test-app/Pipfile new file mode 100644 index 0000000..117b223 --- /dev/null +++ b/test/pipenv-test-app/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.20.0" + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.9" diff --git a/test/pipenv-test-app/Pipfile.lock b/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 0000000..2574173 --- /dev/null +++ b/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,145 @@ +{ + "_meta": { + "hash": { + "sha256": "9d47e9d2d35583e081cf783e9421e75fafb102d07969ad6f74546f48db3a8101" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "gunicorn": { + "hashes": [ + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" + ], + "markers": "python_version >= '3.4'", + "version": "==20.0.4" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "requests": { + "hashes": [ + "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", + "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" + ], + "index": "pypi", + "version": "==2.20.0" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", + "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' and python_version < '4'", + "version": "==1.24.3" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", + "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437", + "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69" + ], + "version": "==1.0.1" + }, + "packaging": { + "hashes": [ + "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8", + "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.4" + }, + "pluggy": { + "hashes": [ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.13.1" + }, + "py": { + "hashes": [ + "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", + "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.9.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9", + "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92" + ], + "index": "pypi", + "version": "==6.1.1" + }, + "six": { + "hashes": [ + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" + }, + "toml": { + "hashes": [ + "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", + "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + ], + "version": "==0.10.1" + } + } +} diff --git a/test/pipenv-test-app/setup.py b/test/pipenv-test-app/setup.py new file mode 100644 index 0000000..e313409 --- /dev/null +++ b/test/pipenv-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/pipenv-test-app/testapp.py b/test/pipenv-test-app/testapp.py new file mode 100644 index 0000000..a9193ee --- /dev/null +++ b/test/pipenv-test-app/testapp.py @@ -0,0 +1,7 @@ +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/test/run b/test/run index cc79666..3a51db2 100755 --- a/test/run +++ b/test/run @@ -6,16 +6,36 @@ # 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=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-cfg,setup-requirements,django,numpy,app-home,npm-virtualenv-uwsgi,locale,mod-wsgi,pipenv,pipenv-and-micropipenv-should-fail,pin-pipenv-version,app-module,micropipenv,micropipenv-requirements}-test-app) # TODO: Make command compatible for Mac users -test_dir="$(readlink -zf $(dirname "${BASH_SOURCE[0]}"))" -image_dir=$(readlink -zf ${test_dir}/..) +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) -# TODO: This should be part of the image metadata -test_port=8080 +test_short_summary='' +TESTSUITE_RESULT=0 + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_scl_variables_in_dockerfile +test_from_dockerfile +" + +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 info() { echo -e "\n\e[1m[INFO] $@\e[0m\n" @@ -29,13 +49,14 @@ 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 + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} } prepare() { @@ -74,31 +95,48 @@ cleanup() { rm -rf ${test_dir}/${1}/.git } +check_type() { + # positive test & non-zero exit status = ERROR + # negative test & zero exit status = ERROR + local result="$1" + local type=${2:-positive} # if not defined, we expect possitive test type (zero exit code) + if [[ "$type" == "positive" && "$result" != "0" ]]; then + info "TEST FAILED (${type}), EXPECTED:0 GOT:${result}" + cleanup + return $result + elif [[ "$type" == "negative" && "$result" == "0" ]]; then + info "TEST FAILED (${type}), EXPECTED: non-zero GOT:${result}" + cleanup + return 1 + fi + +} check_result() { + # Function sets if test suite failed or not + # If return value is not 0 then mark test case as TESTCASE_RESULT=1 local result="$1" if [[ "$result" != "0" ]]; then - info "TEST FAILED (${result})" - cleanup - exit $result + TESTCASE_RESULT=1 fi + return $result } 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 + [ -f $cid_file ] && [ -s $cid_file ] && return 0 attempt=$(( $attempt + 1 )) sleep $sleep_time done + return 1 } 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() { @@ -157,40 +195,136 @@ test_application() { # Wait for the container to write it's CID file wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + check_result $? - 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 $? cleanup_app } +test_from_dockerfile(){ + info "Test from Dockerfile" + # The latest LTS release of Django does not work on RHEL 7 (due to an old version of SQLite), + # and no longer supports Python 2. So on all CentOS/RHEL 7 images and all Python 2 images, + # we're using the old Django LTS version 1.11.x from the main git branch of the django-ex project. + # In all other cases we're using the newer Django LTS version 2.2.x from the corresponding git branch. -# Since we built the candidate image locally, we don't want S2I attempt to pull -# it from Docker hub -s2i_args="--force-pull=false" + if [[ ${VERSION} == "2.7" ]] || docker inspect ${IMAGE_NAME} --format "{{.Config.Env}}" | tr " " "\n" | grep -q "^PLATFORM=el7"; then + django_example_repo_url="https://github.com/sclorg/django-ex.git" + else + django_example_repo_url="https://github.com/sclorg/django-ex.git@2.2.x" + fi -# 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} + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src check_result $? - # test application with default user - test_application + info "Test from Dockerfile with no s2i scripts used" + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile_no_s2i.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + check_result $? +} +test_application_with_user() { # test application with random user - CONTAINER_ARGS="-u 12345" test_application + CONTAINER_ARGS="--user 12345" test_application + +} + +test_application_enable_init_wrapper() { + # test application with init wrapper + CONTAINER_ARGS="-e ENABLE_INIT_WRAPPER=true" test_application +} + +test_scl_variables_in_dockerfile() { + if [ "$OS" == "rhel7" ] || [ "$OS" == "centos7" ]; then + TESTCASE_RESULT=0 + # autocleanup only enabled here as only the following tests so far use it + CID_FILE_DIR=$(mktemp -d) + ct_enable_cleanup + + info "Testing variable presence during \`docker exec\`" + ct_check_exec_env_vars + check_result $? + + info "Checking if all scl variables are defined in Dockerfile" + ct_check_scl_enable_vars + check_result $? + fi +} + +function run_all_tests() { + local APP_NAME=${1:-undefined} + for test_case in $TEST_SET; do + info "Running test $test_case ... " + TESTCASE_RESULT=0 + $test_case + local test_msg + if [ $TESTCASE_RESULT -eq 0 ]; then + test_msg="[PASSED]" + else + test_msg="[FAILED]" + TESTSUITE_RESULT=1 + fi + test "$APP_NAME" == "undefined" && msg_app="" || msg_app="'$APP_NAME'" + printf -v test_short_summary "%s %s for %s %s\n" "${test_short_summary}" "${test_msg}" "${msg_app}" "$test_case" + [ -n "${FAIL_QUICKLY:-}" ] && { + cleanup "${APP_NAME}" + return 1 + } + done; +} + +# For debugging purposes, this script can be run with one or more arguments +# those arguments list is a sub-set of values in the WEB_APPS array defined above +# Example: ./run app-home-test-app pipenv-test-app +for app in ${@:-${WEB_APPS[@]}}; do + # Since we built the candidate image locally, we don't want S2I attempt to pull + # it from Docker hub + s2i_args="--pull-policy=never" + + # Example apps with "-different-port-" in their name don't use the default port 8080 + if [[ "$app" == *"-different-port-"* ]]; then + test_port=8085 + else + test_port=8080 + fi + + prepare ${app} + run_s2i_build ${app} + RESULT=$? + msg_run_s2i_build="'${app}' run_s2i_build" + if [[ "$app" == *"-should-fail-"* ]]; then + # Tests with '-should-fail-' in their name should fail during a build, expecting non-zero exit status + check_type $RESULT "negative" + test "$?" == "0" && test_msg="[PASSED]" || test_msg="[FAILED]" + printf -v test_short_summary "%s %s for %s\n" "${test_short_summary}" "$test_msg" "$msg_run_s2i_build" + continue + else + check_type $RESULT + test "$?" != "0" && test_msg="[FAILED]" || test_msg="[PASSED]" + printf -v test_short_summary "%s %s for %s\n" "${test_short_summary}" "$test_msg" "$msg_run_s2i_build" + fi + echo "" + TEST_SET=${TESTS:-$TEST_LIST} run_all_tests "${app}" - info "All tests for the ${app} finished successfully." cleanup ${app} done -info "All tests finished successfully." +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} run_all_tests + +echo "$test_short_summary" + +if [ $TESTSUITE_RESULT -eq 0 ] ; then + echo "Tests for ${IMAGE_NAME} succeeded." +else + echo "Tests for ${IMAGE_NAME} failed." +fi + +exit $TESTSUITE_RESULT diff --git a/test/run-openshift b/test/run-openshift new file mode 100755 index 0000000..b66294e --- /dev/null +++ b/test/run-openshift @@ -0,0 +1,66 @@ +#!/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]}) +test_dir="$(readlink -f $(dirname ${BASH_SOURCE[0]}))" + +source "${THISDIR}/test-lib.sh" +source "${THISDIR}/test-lib-openshift.sh" +source "${THISDIR}/test-lib-python.sh" + +set -eo nounset + +trap ct_os_cleanup EXIT SIGINT + +test_latest_imagestreams() { + info "Testing the latest version in imagestreams" + # Switch to root directory of a container + pushd "${test_dir}/../.." >/dev/null + ct_check_latest_imagestreams + popd >/dev/null +} +# Pull image before going throw tests +# Exit in case of failure +ct_pull_image "quay.io/centos7/postgresql-10-centos7" "true" + +ct_os_check_compulsory_vars + +ct_os_enable_print_logs + +if [ -z "${EPHEMERAL_TEMPLATES:-}" ]; then + EPHEMERAL_TEMPLATES=" +https://raw.githubusercontent.com/sclorg/django-ex/master/openshift/templates/django-postgresql.json \ +https://raw.githubusercontent.com/openshift/origin/master/examples/quickstarts/django-postgresql.json" +fi + +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' + +for template in $EPHEMERAL_TEMPLATES; do + ct_os_test_template_app "$IMAGE_NAME" \ + "$template" \ + python \ + 'Welcome to your Django application on OpenShift' \ + 8080 http 200 "-p SOURCE_REPOSITORY_REF=master -p PYTHON_VERSION=${VERSION} -p POSTGRESQL_VERSION=10 -p NAME=python-testing" \ + "quay.io/centos7/postgresql-10-centos7|postgresql:10" +done + +# Check the imagestream +test_python_imagestream + +# check if latest imagestream version is correct +test_latest_imagestreams + +OS_TESTSUITE_RESULT=0 + +ct_os_cluster_down + +# vim: set tabstop=2:shiftwidth=2:expandtab: + diff --git a/test/run-openshift-remote-cluster b/test/run-openshift-remote-cluster new file mode 100755 index 0000000..80e9a9c --- /dev/null +++ b/test/run-openshift-remote-cluster @@ -0,0 +1,72 @@ +#!/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" +source "${THISDIR}/test-lib-python.sh" +source "${THISDIR}/test-lib-remote-openshift.sh" + +set -eo nounset + +# Pull image before going throw tests +# Exit in case of failure +ct_pull_image "quay.io/centos7/postgresql-10-centos7" "true" + +ct_os_set_ocp4 + +trap ct_os_cleanup EXIT SIGINT + +ct_os_check_compulsory_vars + +oc status || false "It looks like oc is not properly logged in." + +# For testing on OpenShift 4 we use internal registry +export CT_EXTERNAL_REGISTRY=true + +test -n "${IMAGE_NAME-}" || false 'make sure $IMAGE_NAME is defined' +test -n "${VERSION-}" || false 'make sure $VERSION is defined' + +if [ -z "${EPHEMERAL_TEMPLATES:-}" ]; then + EPHEMERAL_TEMPLATES=" +https://raw.githubusercontent.com/sclorg/django-ex/master/openshift/templates/django-postgresql.json \ +https://raw.githubusercontent.com/openshift/origin/master/examples/quickstarts/django-postgresql.json" +fi + +ct_os_test_s2i_app "${IMAGE_NAME}" "https://github.com/sclorg/s2i-python-container.git" "examples/standalone-test-app" "Hello World from standalone WSGI application!" + +if [[ ${VERSION} == "2.7" ]] || docker inspect ${IMAGE_NAME} --format "{{.Config.Env}}" | tr " " "\n" | grep -q "^PLATFORM=el7"; then + django_example_repo_url="https://github.com/sclorg/django-ex.git" +else + django_example_repo_url="https://github.com/sclorg/django-ex.git#2.2.x" +fi + +ct_os_test_s2i_app "${IMAGE_NAME}" "${django_example_repo_url}" . 'Welcome to your Django application on OpenShift' + +for template in $EPHEMERAL_TEMPLATES; do + if [[ ${VERSION} == "2.7" ]] || docker inspect ${IMAGE_NAME} --format "{{.Config.Env}}" | tr " " "\n" | grep -q "^PLATFORM=el7"; then + branch="master" + else + branch="2.2.x" + fi + ct_os_test_template_app "$IMAGE_NAME" \ + "$template" \ + python \ + 'Welcome to your Django application on OpenShift' \ + 8080 http 200 "-p SOURCE_REPOSITORY_REF=$branch -p PYTHON_VERSION=${VERSION} -p POSTGRESQL_VERSION=10 -p NAME=python-testing" \ + "quay.io/centos7/postgresql-10-centos7|postgresql:10" +done + +# Check the imagestream +test_python_imagestream + +OS_TESTSUITE_RESULT=0 + +# vim: set tabstop=2:shiftwidth=2:expandtab: + diff --git a/test/setup-cfg-test-app/pyproject.toml b/test/setup-cfg-test-app/pyproject.toml new file mode 100644 index 0000000..374b58c --- /dev/null +++ b/test/setup-cfg-test-app/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/test/setup-cfg-test-app/setup.cfg b/test/setup-cfg-test-app/setup.cfg new file mode 100644 index 0000000..a8bedfd --- /dev/null +++ b/test/setup-cfg-test-app/setup.cfg @@ -0,0 +1,14 @@ +[metadata] +name = testapp +version = 0.1 + +[options] +install_requires = + gunicorn<20.0.0; python_version < '3.5' + gunicorn>=20.0.0; python_version >= '3.5' +package_dir = + = . +packages = find: + +[options.packages.find] +where = . diff --git a/test/setup-cfg-test-app/wsgi.py b/test/setup-cfg-test-app/wsgi.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/setup-cfg-test-app/wsgi.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/setup-requirements-test-app/.gitignore b/test/setup-requirements-test-app/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/test/setup-requirements-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-requirements-test-app/requirements.txt b/test/setup-requirements-test-app/requirements.txt new file mode 100644 index 0000000..9368e48 --- /dev/null +++ b/test/setup-requirements-test-app/requirements.txt @@ -0,0 +1,3 @@ +-e . +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/setup-requirements-test-app/setup.py b/test/setup-requirements-test-app/setup.py new file mode 100644 index 0000000..e313409 --- /dev/null +++ b/test/setup-requirements-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-requirements-test-app/testapp.py b/test/setup-requirements-test-app/testapp.py new file mode 100644 index 0000000..09fee1b --- /dev/null +++ b/test/setup-requirements-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/setup-test-app/requirements.txt b/test/setup-test-app/requirements.txt new file mode 100644 index 0000000..268bd60 --- /dev/null +++ b/test/setup-test-app/requirements.txt @@ -0,0 +1,2 @@ +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' \ No newline at end of file diff --git a/test/standalone-test-app/app.py b/test/standalone-test-app/app.py index 5ad0d39..a3403e5 100644 --- a/test/standalone-test-app/app.py +++ b/test/standalone-test-app/app.py @@ -2,7 +2,7 @@ import os import time from gunicorn.app.base import BaseApplication -from gunicorn.six import iteritems +from six import iteritems print('LOADING MODULE %s' % __file__) diff --git a/test/standalone-test-app/requirements.txt b/test/standalone-test-app/requirements.txt index 8f22dcc..d1f415e 100644 --- a/test/standalone-test-app/requirements.txt +++ b/test/standalone-test-app/requirements.txt @@ -1 +1,3 @@ -gunicorn +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' +six \ No newline at end of file diff --git a/test/test-lib-openshift.sh b/test/test-lib-openshift.sh new file mode 100644 index 0000000..96cae8d --- /dev/null +++ b/test/test-lib-openshift.sh @@ -0,0 +1,1368 @@ +# shellcheck shell=bash +# some functions are used from test-lib.sh, that is usually in the same dir +# shellcheck source=/dev/null +source "$(dirname "${BASH_SOURCE[0]}")"/test-lib.sh + +# Set of functions for testing docker images in OpenShift using 'oc' command + +# A variable containing the overall test result; must be changed to 0 in the end +# of the testing script: +# OS_TESTSUITE_RESULT=0 +# And the following trap must be set, in the beginning of the test script: +# trap ct_os_cleanup EXIT SIGINT +OS_TESTSUITE_RESULT=1 +OS_CLUSTER_STARTED_BY_TEST=0 + +function ct_os_cleanup() { + echo "${test_short_summary:-}" + if [ $OS_TESTSUITE_RESULT -eq 0 ] ; then + # shellcheck disable=SC2153 + echo "OpenShift tests for ${IMAGE_NAME} succeeded." + else + # shellcheck disable=SC2153 + echo "OpenShift tests for ${IMAGE_NAME} failed." + fi +} + +# ct_os_check_compulsory_vars +# --------------------------- +# Check the compulsory variables: +# * IMAGE_NAME specifies a name of the candidate image used for testing. +# * VERSION specifies the major version of the MariaDB in format of X.Y +# * OS specifies RHEL version (e.g. OS=rhel7) +function ct_os_check_compulsory_vars() { + # shellcheck disable=SC2016 + test -n "${IMAGE_NAME-}" || ( echo 'make sure $IMAGE_NAME is defined' >&2 ; exit 1) + # shellcheck disable=SC2016 + test -n "${VERSION-}" || ( echo 'make sure $VERSION is defined' >&2 ; exit 1) + # shellcheck disable=SC2016 + test -n "${OS-}" || ( echo 'make sure $OS is defined' >&2 ; exit 1) +} + +# 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 -r 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 + local public_ip='' + local found_ip + hostnames=$(hostname -I) + 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 + # shellcheck disable=SC2001 + 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_build_pod_status POD_PREFIX +# -------------------- +# Returns status of the build pod specified by prefix [pod_prefix]. +# Arguments: pod_prefix - prefix or whole ID of the pod +function ct_os_get_build_pod_status() { + local pod_prefix="${1}" ; shift + local query="custom-columns=NAME:.metadata.name,Ready:status.phase" + oc get pods -o "$query" | grep -e "${pod_prefix}" | grep -E "\-build\s" \ + | sort -u | awk '{print $2}' | tail -n 1 +} + +# ct_os_get_buildconfig_pod_name POD_PREFIX +# ---------------------------- +# Returns status of the buildconfig pod specified by prefix [pod_prefix]. +# Argument: pod_prefix - prefix +function ct_os_get_buildconfig_pod_name() { + local pod_prefix="${1}" ; shift + local query="custom-columns=NAME:.metadata.name" + oc get bc -o "$query" | grep -e "${pod_prefix}" | sort -u | tail -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_get_sti_build_logs +# ----------------- +# Return logs from sti_build +# Arguments: pod_name +function ct_os_get_sti_build_logs() { + local pod_prefix="${1}" + pod_name=$(ct_os_get_buildconfig_pod_name "${pod_prefix}") + # Print logs but do not failed. Just for traces + if [ x"${pod_name}" != "x" ]; then + oc logs "bc/$pod_name" || return 0 + else + echo "Build config bc/$pod_name does not exist for some reason." + echo "Import probably failed." + fi +} + +# 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 + # If there is a build pod - wait for it to finish first + sleep 3 + if ct_os_get_all_pods_name | grep -E "${pod_prefix}.*-build"; then + SECONDS=0 + echo -n "Waiting for ${pod_prefix} build pod to finish ..." + while ! [ "$(ct_os_get_build_pod_status "${pod_prefix}")" == "Succeeded" ] ; do + echo -n "." + if [ "${SECONDS}" -gt "${timeout}0" ]; then + echo " FAIL" + ct_os_print_logs || : + ct_os_get_sti_build_logs "${pod_prefix}" || : + return 1 + fi + sleep 3 + done + echo " DONE" + fi + SECONDS=0 + echo -n "Waiting for ${pod_prefix} pod becoming ready ..." + while ! ct_os_check_pod_readiness "${pod_prefix}" "true" ; do + echo -n "." + if [ "${SECONDS}" -gt "${timeout}" ]; then + echo " FAIL"; + ct_os_print_logs || : + ct_os_get_sti_build_logs "${pod_prefix}" || : + return 1 + fi + 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} having desired numbers of replicas ..." + 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 "." + if [ "${SECONDS}" -gt "${timeout}" ]; then + echo " FAIL"; + ct_os_print_logs || : + ct_os_get_sti_build_logs "${pod_prefix}" || : + return 1 + fi + 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}" --strategy=source "$@" || : + + # 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. +# The OPENSHIFT_CLUSTER_PULLSECRET_PATH environment variable can be set +# to contain a path to a k8s secret definition which will be used +# to authenticate to image registries. +# shellcheck disable=SC2120 +function ct_os_new_project() { + if [ "${CVP:-0}" -eq "1" ]; then + echo "Testing in CVP environment. No need to create OpenShift project. This is done by CVP pipeline" + return + fi + 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 + if test -n "${OPENSHIFT_CLUSTER_PULLSECRET_PATH:-}" -a -e "${OPENSHIFT_CLUSTER_PULLSECRET_PATH:-}"; then + oc create -f "$OPENSHIFT_CLUSTER_PULLSECRET_PATH" + # add registry pullsecret to the serviceaccount if provided + secret_name=$(grep '^\s*name:' "$OPENSHIFT_CLUSTER_PULLSECRET_PATH" | awk '{ print $2 }') + oc secrets link --for=pull default "$secret_name" + fi +} + +# ct_os_delete_project [PROJECT] +# -------------------- +# Deletes the specified project in the openshfit +# Arguments: project - project name, uses the current project if omitted +# shellcheck disable=SC2120 +function ct_os_delete_project() { + if [ "${CT_SKIP_NEW_PROJECT:-false}" == 'true' ] || [ "${CVP:-0}" -eq "1" ]; then + echo "Deleting project skipped, cleaning objects only." + # when not having enough privileges (remote cluster), it might fail and + # it is not a big problem, so ignore failure in this case + ct_delete_all_objects || : + return + fi + local project_name="${1:-$(oc project -q)}" ; shift || : + if oc delete project "${project_name}" ; then + echo "Project ${project_name} was deleted properly" + else + echo "Project ${project_name} was not delete properly. But it does not block CI." + fi + +} + +# 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 + # shellcheck disable=SC2034 + for i in $(seq 12) ; do + # shellcheck disable=SC2015 + 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 + + 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_is_tag_exists IS_NAME TAG +# -------------------- +# Checks whether the specified tag exists for an image stream +# Arguments: is_name - name of the image stream +# Arguments: tag - name of the tag (usually version) +function ct_os_is_tag_exists() { + local is_name=$1 ; shift + local tag=$1 ; shift + oc get is "${is_name}" -n openshift -o=jsonpath='{.spec.tags[*].name}' | grep -qw "${tag}" +} + +# ct_os_template_exists T_NAME +# -------------------- +# Checks whether the specified template exists for an image stream +# Arguments: t_name - template name of the image stream +function ct_os_template_exists() { + local t_name=$1 ; shift + oc get templates -n openshift | grep -q "^${t_name}\s" +} + +# 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 + OS_CLUSTER_STARTED_BY_TEST=1 + # 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() { + if [ ${OS_CLUSTER_STARTED_BY_TEST:-0} -eq 1 ] ; then + echo "Switching user to system:admin before cluster is going down." + oc login -u system:admin + echo "Cluster started by the test, shutting down." + oc cluster down + else + echo "Cluster not started by the test, shutting down skipped." + fi +} + +# 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-/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 + local oc_path + + oc_version=$(ct_os_get_latest_ver "$1") + + if oc version | grep -q "oc ${oc_version%.*}." ; then + echo "Binary oc found already available in version ${oc_version}: $(command -v 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}" ] ; 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}: $(command -v 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 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; '' 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 image_name_no_namespace=${image_name##*/} + local service_name="${image_name_no_namespace%%:*}-testing" + local namespace + + if [ $# -lt 4 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] || [ -z "${4}" ]; then + echo "ERROR: ct_os_test_s2i_app_func() requires at least 4 arguments that cannot be emtpy." >&2 + return 1 + fi + + # shellcheck disable=SC2119 + ct_os_new_project + + namespace=${CT_NAMESPACE:-"$(oc project -q)"} + local image_tagged="${image_name_no_namespace%:*}:${VERSION}" + + if [ "${CVP:-0}" -eq "0" ]; then + if [ "${CT_EXTERNAL_REGISTRY:-false}" == 'true' ] ; then + ct_os_import_image_ocp4 "${image_name}" "${image_tagged}" + else + # Create a specific imagestream tag for the image so that oc cannot use anything else + if [ "${CT_SKIP_UPLOAD_IMAGE:-false}" == 'true' ] ; then + echo "Importing image ${image_name} as ${namespace}/${image_tagged}" + # Use --reference-policy=local to pull remote image content to the cluster + # Works around the issue of builder pods not having access to registry.redhat.io + oc tag --source=docker "${image_name}" "${namespace}/${image_tagged}" --insecure=true --reference-policy=local + ct_os_wait_stream_ready "${image_tagged}" "${namespace}" + else + echo "Uploading image ${image_name} as ${image_tagged}" + ct_os_upload_image "${image_name}" "${image_tagged}" + fi + fi + else + echo "Testing image ${image_name} in CVP pipeline." + 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 + + # shellcheck disable=SC2086 + 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 + local check_command_exp + local image_id + + # get image ID from the deployment config + image_id=$(oc get "deploymentconfig.apps.openshift.io/${service_name}" -o custom-columns=IMAGE:.spec.template.spec.containers[*].image | tail -n 1) + + ip=$(ct_os_get_service_ip "${service_name}") + # shellcheck disable=SC2001 + check_command_exp=$(echo "$check_command" | sed -e "s//$ip/g" -e "s||${image_id}|g") + + echo " Checking APP using $check_command_exp ..." + local result=0 + eval "$check_command_exp" || result=1 + + ct_os_service_image_info "${service_name}" + + if [ $result -eq 0 ] ; then + echo " Check passed." + else + echo " Check failed." + fi + + # shellcheck disable=SC2119 + 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:-} + + if [ $# -lt 4 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] || [ -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}://:${port}' '${response_code}' '${expected_output}'" \ + "${oc_args}" +} + +# 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 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; '' 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 "|", where "" is a full image name +# (including registry if needed) and "" 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:-} + + if [ $# -lt 4 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] || [ -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}" + local namespace + + # shellcheck disable=SC2119 + ct_os_new_project + + namespace=${CT_NAMESPACE:-"$(oc project -q)"} + # Upload main image is already done by CVP pipeline. No need to do it twice. + if [ "${CVP:-0}" -eq "0" ]; then + # Create a specific imagestream tag for the image so that oc cannot use anything else + if [ "${CT_EXTERNAL_REGISTRY:-false}" == 'true' ] ; then + ct_os_import_image_ocp4 "${image_name}" "${image_tagged}" + else + if [ "${CT_SKIP_UPLOAD_IMAGE:-false}" == 'true' ] ; then + echo "Importing image ${image_name} as ${image_tagged}" + # Use --reference-policy=local to pull remote image content to the cluster + # Works around the issue of builder pods not having access to registry.redhat.io + oc tag --source=docker "${image_name}" "${namespace}/${image_tagged}" --insecure=true --reference-policy=local + ct_os_wait_stream_ready "${image_tagged}" "${namespace}" + else + echo "Uploading image ${image_name} as ${image_tagged}" + ct_os_upload_image "${image_name}" "${image_tagged}" + fi + fi + else + echo "Import is already done by CVP pipeline." + fi + # Other images are not uploaded by CVP pipeline. We need to do it. + if [ "${CT_SKIP_UPLOAD_IMAGE:-false}" == 'false' ] ; then + # upload also other images, that template might need (list of pairs in the format | + local image_tag_a + local i_t + for i_t in ${other_images} ; do + echo "${i_t}" + IFS='|' read -ra image_tag_a <<< "${i_t}" + if [[ "$(docker images -q "$image_name" 2>/dev/null)" == "" ]]; then + echo "ERROR: Image $image_name is not pulled yet." + docker images + echo "Add to the beginning of scripts run-openshift-remote-cluster and run-openshift row" + echo "'ct_pull_image $image_name true'." + exit 1 + fi + + if [ "${CT_EXTERNAL_REGISTRY:-false}" == 'true' ] ; then + ct_os_import_image_ocp4 "${image_tag_a[0]}" "${image_tag_a[1]}" + else + ct_os_upload_image "${image_tag_a[0]}" "${image_tag_a[1]}" + fi + done + fi + + # get the template file from remote or local location; if not found, it is + # considered an internal template name, like 'mysql', so use the name + # explicitly + local local_template + + local_template=$(ct_obtain_input "${template}" 2>/dev/null || echo "--template=${template}") + + echo "Creating a new-app with name ${name_in_template} in namespace ${namespace} with args ${oc_args}." + # shellcheck disable=SC2086 + oc new-app "${local_template}" \ + --name "${name_in_template}" \ + -p NAMESPACE="${namespace}" \ + ${oc_args} + + ct_os_wait_pod_ready "${service_name}" 300 + + local ip + local check_command_exp + local image_id + + # get image ID from the deployment config + image_id=$(oc get "deploymentconfig.apps.openshift.io/${service_name}" -o custom-columns=IMAGE:.spec.template.spec.containers[*].image | tail -n 1) + + ip=$(ct_os_get_service_ip "${service_name}") + # shellcheck disable=SC2001 + check_command_exp=$(echo "$check_command" | sed -e "s//$ip/g" -e "s||${image_id}|g") + + echo " Checking APP using $check_command_exp ..." + local result=0 + eval "$check_command_exp" || result=1 + + ct_os_service_image_info "${service_name}" + + if [ $result -eq 0 ] ; then + echo " Check passed." + else + echo " Check failed." + fi + + # shellcheck disable=SC2119 + 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 "|", where "" is a full image name +# (including registry if needed) and "" 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:-} + + if [ $# -lt 4 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ] || [ -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}://:${port}' '${response_code}' '${expected_output}'" \ + "${oc_args}" \ + "${other_images}" +} + +# 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 ip="" check_command_exp="" + local image_name_no_namespace=${image_name##*/} + local service_name="${image_name_no_namespace%%:*}-testing" + + echo "Running image update test for: $image_name" + # shellcheck disable=SC2119 + 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} + 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} + ct_assert_cmd_success "$check_command_exp" + + # shellcheck disable=SC2119 + 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 ${image_name}" + oc create -f - <" + local sleep_time=3 + local attempt=1 + local result=1 + local status + local response_code + local response_file + local util_image_name='registry.access.redhat.com/ubi7/ubi' + + response_file=$(mktemp /tmp/ct_test_response_XXXXXX) + 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=$(tail -c 3 "${response_file}") + if [ "${response_code}" -eq "${expected_code}" ]; then + result=0 + fi + grep -qP -e "${body_regexp}" "${response_file}" || 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 ] || [ "${attempt}" -gt "${ignore_error_attempts}" ] || [ "${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 + 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 + local check_command_exp + + ip=$(ct_os_get_service_ip "${service_name}") + # shellcheck disable=SC2001 + check_command_exp=$(echo "$check_command" | sed -e "s//$ip/g") + + ct_os_deploy_cmd_image "${util_image_name}" + 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 +} + +# ct_os_test_image_stream_template IMAGE_STREAM_FILE TEMPLATE_FILE SERVICE NAME [TEMPLATE_PARAMS] +# ------------------------ +# Creates an image stream and deploys a specified template. Then checks that a pod runs. +# Argument: image_stream_file - local or remote file with the image stream definition +# Argument: template_file - local file name with a template +# Argument: service_name - how the pod will be named (prefix) +# Argument: template_params (optional) - parameters for the template, like image stream version +function ct_os_test_image_stream_template() { + local image_stream_file=${1} + local template_file=${2} + local service_name=${3} + local template_params=${4:-} + local local_image_stream_file + local local_template_file + + if [ $# -lt 3 ] || [ -z "${1}" ] || [ -z "${2}" ] || [ -z "${3}" ]; then + echo "ERROR: ct_os_test_image_stream() requires at least 3 arguments that cannot be empty." >&2 + return 1 + fi + + echo "Running image stream test for stream ${image_stream_file} and template ${template_file}" + # shellcheck disable=SC2119 + ct_os_new_project + + local_image_stream_file=$(ct_obtain_input "${image_stream_file}") + local_template_file=$(ct_obtain_input "${template_file}") + oc create -f "${local_image_stream_file}" + + # shellcheck disable=SC2086 + if ! ct_os_deploy_template_image "${local_template_file}" -p NAMESPACE="${CT_NAMESPACE:-$(oc project -q)}" ${template_params} ; then + echo "ERROR: ${template_file} could not be loaded" + return 1 + # Deliberately not runnig ct_os_delete_project here because user either + # might want to investigate or the cleanup is done with the cleanup trap. + # Most functions depend on the set -e anyway at this point. + fi + ct_os_wait_pod_ready "${service_name}" 120 + + # shellcheck disable=SC2119 + ct_os_delete_project +} + +# ct_os_wait_stream_ready IMAGE_STREAM_FILE NAMESPACE [ TIMEOUT ] +# ------------------------ +# Waits max timeout seconds till a [stream] is available in the [namespace]. +# Arguments: image_stream - stream name (usuallly :) +# Arguments: namespace - namespace name +# Arguments: timeout - how many seconds to wait +function ct_os_wait_stream_ready() { + local image_stream=${1} + local namespace=${2} + local timeout=${3:-60} + # It takes some time for the first time before the image is pulled in + SECONDS=0 + echo -n "Waiting for ${namespace}/${image_stream} to become available ..." + while ! oc get -n "${namespace}" istag "${image_stream}" &>/dev/null; do + if [ "$SECONDS" -gt "${timeout}" ] ; then + echo "FAIL: ${namespace}/${image_stream} not available after ${timeout}s:" + echo "oc get -n ${namespace} istag ${image_stream}" + oc get -n "${namespace}" istag "${image_stream}" + return 1 + fi + sleep 3 + echo -n . + done + echo " DONE" +} + +# ct_os_test_image_stream_s2i IMAGE_STREAM_FILE IMAGE_NAME APP CONTEXT_DIR EXPECTED_OUTPUT [PORT, PROTOCOL, RESPONSE_CODE, OC_ARGS, ... ] +# -------------------- +# Check the imagestream with an s2i app check. First it imports the given image stream, then +# it runs [image] and [app] in the openshift and optionally specifies env_params +# as environment variables to the image. Then check the http response. +# Argument: image_stream_file - local or remote file with the image stream definition +# Argument: image_name - container image we test (or name of the existing image stream in : format) +# Argument: app - url or local path to git repo with the application sources (compulsory) +# Argument: context_dir - sub-directory inside the repository with the application sources (compulsory) +# Argument: expected_output - PCRE regular expression that must match the response body (compulsory) +# Argument: port - which port to use (optional; default: 8080) +# Argument: protocol - which protocol to use (optional; default: http) +# Argument: response_code - what http response code to expect (optional; default: 200) +# Argument: oc_args - all other arguments are used as additional parameters for the `oc new-app` +# command, typically environment variables (optional) +function ct_os_test_image_stream_s2i() { + local image_stream_file=${1} + local image_name=${2} + local app=${3} + local context_dir=${4} + local expected_output=${5} + local port=${6:-8080} + local protocol=${7:-http} + local response_code=${8:-200} + local oc_args=${9:-} + local result + local local_image_stream_file + + echo "Running image stream test for stream ${image_stream_file} and application ${app} with context ${context_dir}" + + # shellcheck disable=SC2119 + ct_os_new_project + + local_image_stream_file=$(ct_obtain_input "${image_stream_file}") + oc create -f "${local_image_stream_file}" + + # ct_os_test_s2i_app creates a new project, but we already need + # it before for the image stream import, so tell it to skip this time + CT_SKIP_NEW_PROJECT=true \ + ct_os_test_s2i_app "${IMAGE_NAME}" "${app}" "${context_dir}" "${expected_output}" \ + "${port}" "${protocol}" "${response_code}" "${oc_args}" + result=$? + + # shellcheck disable=SC2119 + ct_os_delete_project + + return $result +} + +# ct_os_test_image_stream_quickstart IMAGE_STREAM_FILE TEMPLATE IMAGE_NAME NAME_IN_TEMPLATE EXPECTED_OUTPUT [PORT, PROTOCOL, RESPONSE_CODE, OC_ARGS, OTHER_IMAGES ] +# -------------------- +# Check the imagestream with an s2i app check. First it imports the given image stream, then +# it runs [image] and [app] in the openshift and optionally specifies env_params +# as environment variables to the image. Then check the http response. +# Argument: image_stream_file - local or remote file with the image stream definition +# Argument: template_file - local file name with a template +# Argument: image_name - container image we test (or name of the existing image stream in : format) +# Argument: name_in_template - image name used in the template +# Argument: expected_output - PCRE regular expression that must match the response body (compulsory) +# Argument: port - which port to use (optional; default: 8080) +# Argument: protocol - which protocol to use (optional; default: http) +# Argument: response_code - what http response code to expect (optional; default: 200) +# Argument: oc_args - all other arguments are used as additional parameters for the `oc new-app` +# command, typically environment variables (optional) +# Argument: other_images - some templates need other image to be pushed into the OpenShift registry, +# specify them in this parameter as "|", where "" is a full image name +# (including registry if needed) and "" is a tag under which the image should be available +# in the OpenShift registry. +function ct_os_test_image_stream_quickstart() { + local image_stream_file=${1} + local template_file=${2} + local image_name=${3} + local name_in_template=${4} + local expected_output=${5} + local port=${6:-8080} + local protocol=${7:-http} + local response_code=${8:-200} + local oc_args=${9:-} + local other_images=${10:-} + local result + local local_image_stream_file + local local_template_file + + echo "Running image stream test for stream ${image_stream_file} and quickstart template ${template_file}" + echo "Image name is ${IMAGE_NAME}" + # shellcheck disable=SC2119 + ct_os_new_project + + local_image_stream_file=$(ct_obtain_input "${image_stream_file}") + local_template_file=$(ct_obtain_input "${template_file}") + # ct_os_test_template_app creates a new project, but we already need + # it before for the image stream import, so tell it to skip this time + namespace=${CT_NAMESPACE:-"$(oc project -q)"} + + # Add namespace into openshift arguments + if [[ $oc_args != *"NAMESPACE"* ]]; then + oc_args="${oc_args} -p NAMESPACE=${namespace}" + fi + oc create -f "${local_image_stream_file}" + + # In case we are testing on OpenShift 4 export variable for mirror image + # which means, that image is going to be mirrored from an internal registry into OpenShift 4 + if [ "${CT_EXTERNAL_REGISTRY:-false}" == 'true' ]; then + export CT_TAG_IMAGE=true + fi + # ct_os_test_template_app creates a new project, but we already need + # it before for the image stream import, so tell it to skip this time + + CT_SKIP_NEW_PROJECT=true \ + ct_os_test_template_app "${image_name}" \ + "${local_template_file}" \ + "${name_in_template}" \ + "${expected_output}" \ + "${port}" "${protocol}" "${response_code}" "${oc_args}" "${other_images}" + + result=$? + + # shellcheck disable=SC2119 + ct_os_delete_project + + return $result +} + +# ct_os_service_image_info SERVICE_NAME +# -------------------- +# Shows information about the image used by a specified service. +# Argument: service_name - Service name (uesd for deployment config) +function ct_os_service_image_info() { + local service_name=$1 + local image_id + local namespace + + # get image ID from the deployment config + image_id=$(oc get "deploymentconfig.apps.openshift.io/${service_name}" -o custom-columns=IMAGE:.spec.template.spec.containers[*].image | tail -n 1) + namespace=${CT_NAMESPACE:-"$(oc project -q)"} + + echo " Information about the image we work with:" + oc get deploymentconfig.apps.openshift.io/"${service_name}" -o yaml | grep lastTriggeredImage + # for s2i builds, the resulting image is actually in the current namespace, + # so if the specified namespace does not succeed, try the current namespace + oc get isimage -n "${namespace}" "${image_id##*/}" -o yaml || oc get isimage "${image_id##*/}" -o yaml +} +# vim: set tabstop=2:shiftwidth=2:expandtab: diff --git a/test/test-lib-python.sh b/test/test-lib-python.sh new file mode 100644 index 0000000..09f0baf --- /dev/null +++ b/test/test-lib-python.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# Functions for tests for the Python image in OpenShift. +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# 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" + +# Check the imagestream +function test_python_imagestream() { + case ${OS} in + rhel7|centos7) ;; + *) echo "Imagestream testing not supported for $OS environment." ; return 0 ;; + esac + + ct_os_test_image_stream_quickstart "${THISDIR}/imagestreams/python-${OS%[0-9]*}.json" \ + 'https://raw.githubusercontent.com/sclorg/django-ex/master/openshift/templates/django-postgresql.json' \ + "${IMAGE_NAME}" \ + 'python' \ + 'Welcome to your Django application on OpenShift' \ + 8080 http 200 "-p SOURCE_REPOSITORY_REF=master -p PYTHON_VERSION=${VERSION} -p POSTGRESQL_VERSION=10 -p NAME=python-testing" \ + "quay.io/centos7/postgresql-10-centos7|postgresql:10" +} + +# vim: set tabstop=2:shiftwidth=2:expandtab: + diff --git a/test/test-lib-remote-openshift.sh b/test/test-lib-remote-openshift.sh new file mode 100644 index 0000000..bda03f6 --- /dev/null +++ b/test/test-lib-remote-openshift.sh @@ -0,0 +1,116 @@ +# shellcheck shell=bash +# some functions are used from test-lib.sh, that is usually in the same dir +# shellcheck source=/dev/null +source "$(dirname "${BASH_SOURCE[0]}")"/test-lib.sh + +# Set of functions for testing docker images in OpenShift using 'oc' command + +# A variable containing the overall test result; must be changed to 0 in the end +# of the testing script: +# OS_TESTSUITE_RESULT=0 +# And the following trap must be set, in the beginning of the test script: +# trap ct_os_cleanup EXIT SIGINT + +# ct_os_set_path_oc_4 OC_VERSION +# -------------------- +# This is a trick that helps using correct version 4 of the `oc`: +# The input is version of the openshift in format 4.4 etc. +# If the currently available version of oc is not of this version, +# it first takes a look into /usr/local/oc-/bin directory, + +# Arguments: oc_version - X.Y part of the version of OSE (e.g. 3.9) +function ct_os_set_path_oc_4() { + echo "Setting OCP4 client" + local oc_version=$1 + local installed_oc_path="/usr/local/oc-v${oc_version}/bin" + echo "PATH ${installed_oc_path}" + if [ -x "${installed_oc_path}/oc" ] ; then + oc_path="${installed_oc_path}" + echo "Binary oc found in ${installed_oc_path}" >&2 + else + echo "OCP4 not found" + return 1 + fi + export PATH="${oc_path}:${PATH}" + oc version + if ! oc version | grep -q "Client Version: ${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}: $(command -v oc)" + fi +} + +# ct_os_prepare_ocp4 +# ------------------ +# Prepares environment for testing images in OpenShift 4 environment +# +# +function ct_os_set_ocp4() { + if [ "${CVP:-0}" -eq "1" ]; then + echo "Testing in CVP environment. No need to login to OpenShift cluster. This is already done by CVP pipeline." + return + fi + local login + OS_OC_CLIENT_VERSION=${OS_OC_CLIENT_VERSION:-4.4} + ct_os_set_path_oc_4 "${OS_OC_CLIENT_VERSION}" + + oc version + + login=$(cat "$KUBEPASSWORD") + oc login -u kubeadmin -p "$login" + echo "Login to OpenShift ${OS_OC_CLIENT_VERSION} is DONE" + # let openshift cluster to sync to avoid some race condition errors + sleep 3 +} + +function ct_os_upload_image_external_registry() { + local input_name="${1}" ; shift + local image_name=${input_name##*/} + local imagestream=${1:-$image_name:latest} + local output_name + + ct_os_login_external_registry + + output_name="${INTERNAL_DOCKER_REGISTRY}/rhscl-ci-testing/$imagestream" + + docker images + docker tag "${input_name}" "${output_name}" + docker push "${output_name}" +} + + +function ct_os_login_external_registry() { + local docker_token + # docker login fails with "404 page not found" error sometimes, just try it more times + # shellcheck disable=SC2034 + echo "loging" + [ -z "${INTERNAL_DOCKER_REGISTRY:-}" ] && "INTERNAL_DOCKER_REGISTRY has to be set for working with Internal registry" && return 1 + # shellcheck disable=SC2034 + for i in $(seq 12) ; do + # shellcheck disable=SC2015 + docker_token=$(cat "$DOCKER_UPSHIFT_TOKEN") + # shellcheck disable=SC2015 + docker login -u rhscl-ci-testing -p "$docker_token" "${INTERNAL_DOCKER_REGISTRY}" && return 0 || : + sleep 5 + done + return 1 +} + +function ct_os_import_image_ocp4() { + local image_name="${1}"; shift + local imagestream=${1:-$image_name:latest} + local namespace + + namespace=${CT_NAMESPACE:-"$(oc project -q)"} + deploy_image_name="${INTERNAL_DOCKER_REGISTRY}/rhscl-ci-testing/${imagestream}" + echo "Uploading image ${image_name} as ${deploy_image_name} , ${imagestream} into external registry." + ct_os_upload_image_external_registry "${image_name}" "${imagestream}" + if [ "${CT_TAG_IMAGE:-false}" == 'true' ]; then + echo "Tag ${deploy_image_name} to ${namespace}/${imagestream}" + oc tag --source=docker "${deploy_image_name}" "${namespace}/${imagestream}" --insecure=true --reference-policy=local + else + echo "Import image into OpenShift 4 environment ${namespace}/${imagestream} from ${deploy_image_name}" + oc import-image "${namespace}/${imagestream}" --from="${deploy_image_name}" --confirm --reference-policy=local + fi +} diff --git a/test/test-lib.sh b/test/test-lib.sh new file mode 100644 index 0000000..e954053 --- /dev/null +++ b/test/test-lib.sh @@ -0,0 +1,1072 @@ +# shellcheck shell=bash +# +# Test a container image. +# +# Always use sourced from a specific container testfile +# +# reguires definition of CID_FILE_DIR +# CID_FILE_DIR=$(mktemp --suffix=_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() { + ct_show_resources + for cid_file in "$CID_FILE_DIR"/* ; do + [ -f "$cid_file" ] || continue + local container + 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_pull_image +# ------------- +# Function pull an image before tests execution +# Argument: image_name - string containing the public name of the image to pull +# Argument: exit - in case "true" is defined and pull failed, then script has to exit with 1 and no tests are executed +# Argument: loops - how many times to pull image in case of failure +# Function returns either 0 in case of pull was successful +# Or the test suite exit with 1 in case of pull error +function ct_pull_image() { + local image_name="$1"; [[ $# -gt 0 ]] && shift + local exit_variable=${1:-"false"}; [[ $# -gt 0 ]] && shift + local loops=${1:-10} + local loop=0 + + # Let's try to pull image. + echo "-> Pulling image $image_name ..." + # Sometimes in Fedora case it fails with HTTP 50X + # Check if the image is available locally and try to pull it if it is not + if [[ "$(docker images -q "$image_name" 2>/dev/null)" != "" ]]; then + echo "The image $image_name is already pulled." + return 0 + fi + + # Try pulling the image to see if it is accessible + # WORKAROUND: Since Fedora registry sometimes fails randomly, let's try it more times + while ! docker pull "$image_name"; do + ((loop++)) || : + echo "Pulling image $image_name failed." + if [ "$loop" -gt "$loops" ]; then + echo "Pulling of image $image_name failed $loops times in a row. Giving up." + echo "!!! ERROR with pulling image $image_name !!!!" + # shellcheck disable=SC2268 + if [[ x"$exit_variable" == x"false" ]]; then + return 1 + else + exit 1 + fi + fi + echo "Let's wait $((loop*5)) seconds and try again." + sleep "$((loop*5))" + done +} + + +# ct_check_envs_set env_filter check_envs loop_envs [env_format] +# -------------------- +# Compares values from one list of environment variable definitions against such list, +# checking if the values are present and have a specific format. +# Argument: env_filter - optional string passed to grep used for +# choosing which variables to filter out in env var lists. +# Argument: check_envs - list of env var definitions to check values against +# Argument: loop_envs - list of env var definitions to check values for +# Argument: env_format (optional) - format string for bash substring deletion used +# for checking whether the value is contained in check_envs. +# Defaults to: "*VALUE*", VALUE string gets replaced by actual value from loop_envs +function ct_check_envs_set { + local env_filter check_envs env_format + env_filter=$1; shift + check_envs=$1; shift + loop_envs=$1; shift + env_format=${1:-"*VALUE*"} + while read -r variable; do + [ -z "$variable" ] && continue + var_name=$(echo "$variable" | awk -F= '{ print $1 }') + stripped=$(echo "$variable" | awk -F= '{ print $2 }') + filtered_envs=$(echo "$check_envs" | grep "^$var_name=") + [ -z "$filtered_envs" ] && { echo "$var_name not found during \` docker exec\`"; return 1; } + old_IFS=$IFS + # For each such variable compare its content with the `docker exec` result, use `:` as delimiter + IFS=: + for value in $stripped; do + # If the falue checked does not go through env_filter we do not care about it + echo "$value" | grep -q "$env_filter" || continue + if [ -n "${filtered_envs##${env_format//VALUE/$value}}" ]; then + echo " Value $value is missing from variable $var_name" + echo "$filtered_envs" + IFS=$old_IFS + return 1 + fi + done + IFS=$old_IFS + done <<< "$(echo "$loop_envs" | grep "$env_filter" | grep -v "^PWD=")" +} + +# 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 + 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-}" + # we really work with CONTAINER_ARGS as with a string + # shellcheck disable=SC2124 + CONTAINER_ARGS="$@" + if ct_create_container "$cid_file" ; then + local cid + 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 + [ -n "$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 + # shellcheck disable=SC2086,SC2153 + 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 + tmpdir=$(mktemp -d) + local f + : " Testing documentation in the container image" + # Extract the help files from the container + # shellcheck disable=SC2043 + 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 ! grep -F -q -e "${term}" "${tmpdir}/$(basename "${f}")" ; 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!" +} + +# full_ca_file_path +# Return string for full path to CA file +function full_ca_file_path() +{ + echo "/etc/pki/ca-trust/source/anchors/RH-IT-Root-CA.crt" +} +# ct_mount_ca_file +# ------------------ +# Check if /etc/pki/certs/RH-IT-Root-CA.crt file exists +# return mount string for containers or empty string +function ct_mount_ca_file() +{ + # mount CA file only if NPM_REGISTRY variable is present. + local mount_parameter="" + if [ -n "$NPM_REGISTRY" ] && [ -f "$(full_ca_file_path)" ]; then + mount_parameter="-v $(full_ca_file_path):$(full_ca_file_path):Z" + fi + echo "$mount_parameter" +} + +# ct_build_s2i_npm_variables URL_TO_NPM_JS_SERVER +# ------------------------------------------ +# Function returns -e NPM_MIRROR and -v MOUNT_POINT_FOR_CAFILE +# or empty string +function ct_build_s2i_npm_variables() +{ + npm_variables="" + if [ -n "$NPM_REGISTRY" ] && [ -f "$(full_ca_file_path)" ]; then + npm_variables="-e NPM_MIRROR=$NPM_REGISTRY $(ct_mount_ca_file)" + fi + echo "$npm_variables" +} + +# ct_npm_works +# -------------------- +# Checks existance of the npm tool and runs it. +function ct_npm_works() { + local tmpdir + tmpdir=$(mktemp -d) + : " Testing npm in the container image" + local cid_file="${tmpdir}/cid" + if ! docker run --rm "${IMAGE_NAME}" /bin/bash -c "npm --version" >"${tmpdir}/version" ; then + echo "ERROR: 'npm --version' does not work inside the image ${IMAGE_NAME}." >&2 + return 1 + fi + + # shellcheck disable=SC2046 + docker run -d $(ct_mount_ca_file) --rm --cidfile="$cid_file" "${IMAGE_NAME}-testapp" + + # Wait for the container to write it's CID file + ct_wait_for_cid "$cid_file" || return 1 + + if ! docker exec "$(cat "$cid_file")" /bin/bash -c "npm --verbose install jquery && test -f node_modules/jquery/src/jquery.js" >"${tmpdir}/jquery" 2>&1 ; then + echo "ERROR: npm could not install jquery inside the image ${IMAGE_NAME}." >&2 + cat "${tmpdir}/jquery" + return 1 + fi + + if [ -n "$NPM_REGISTRY" ] && [ -f "$(full_ca_file_path)" ]; then + if ! grep -qo "$NPM_REGISTRY" "${tmpdir}/jquery"; then + echo "ERROR: Internal repository is NOT set. Even it is requested." + return 1 + fi + fi + + if [ -f "$cid_file" ]; then + docker stop "$(cat "$cid_file")" + rm "$cid_file" + fi + : " Success!" +} + +# ct_binary_found_from_df binary [path] +# -------------------- +# Checks if a binary can be found in PATH during Dockerfile build +# Argument: binary - name of the binary to test accessibility for +# Argument: path - optional path in which the binary should reside in +# /opt/rh by default +function ct_binary_found_from_df() { + local tmpdir + local binary=$1; shift + local binary_path=${1:-"^/opt/rh"} + tmpdir=$(mktemp -d) + : " Testing $binary in build from Dockerfile" + + # Create Dockerfile that looks for the binary + cat <"$tmpdir/Dockerfile" +FROM $IMAGE_NAME +RUN command -v $binary | grep "$binary_path" +EOF + # Build an image, looking for expected path in the output + if ! docker build -f "$tmpdir/Dockerfile" --no-cache "$tmpdir"; then + echo " ERROR: Failed to find $binary in Dockerfile!" >&2 + return 1 + fi + : " Success!" +} + +# ct_check_exec_env_vars [env_filter] +# -------------------- +# Checks if all relevant environment variables from `docker run` +# can be found in `docker exec` as well. +# Argument: env_filter - optional string passed to grep used for +# choosing which variables to check in the test case. +# Defaults to X_SCLS and variables containing /opt/app-root, /opt/rh +# Uses: $CID_FILE_DIR - path to directory containing cid_files +# Uses: $IMAGE_NAME - name of the image being tested +function ct_check_exec_env_vars() { + local tmpdir exec_envs cid old_IFS env_filter + local var_name stripped filtered_envs run_envs + env_filter=${1:-"^X_SCLS=\|/opt/rh\|/opt/app-root"} + tmpdir=$(mktemp -d) + CID_FILE_DIR=${CID_FILE_DIR:-$(mktemp -d)} + # Get environment variables from `docker run` + run_envs=$(docker run --rm "$IMAGE_NAME" /bin/bash -c "env") + # Get environment variables from `docker exec` + ct_create_container "test_exec_envs" bash -c "sleep 1000" >/dev/null + cid=$(ct_get_cid "test_exec_envs") + exec_envs=$(docker exec "$cid" env) + # Filter out variables we are not interested in + # Always check X_SCLS, ignore PWD + # Check variables from `docker run` that have alternative paths inside (/opt/rh, /opt/app-root) + ct_check_envs_set "$env_filter" "$exec_envs" "$run_envs" "*VALUE*" || return 1 + echo " All values present in \`docker exec\`" + return 0 +} + +# ct_check_scl_enable_vars [env_filter] +# -------------------- +# Checks if all relevant environment variables from `docker run` +# are set twice after a second call of `scl enable $SCLS`. +# Argument: env_filter - optional string passed to grep used for +# choosing which variables to check in the test case. +# Defaults to paths containing enabled SCLS in the image +# Uses: $IMAGE_NAME - name of the image being tested +function ct_check_scl_enable_vars() { + local tmpdir exec_envs cid old_IFS env_filter enabled_scls + local var_name stripped filtered_envs loop_envs + env_filter=$1 + tmpdir=$(mktemp -d) + enabled_scls=$(docker run --rm "$IMAGE_NAME" /bin/bash -c "echo \$X_SCLS") + if [ -z "$env_filter" ]; then + for scl in $enabled_scls; do + [ -z "$env_filter" ] && env_filter="/$scl" && continue + # env_filter not empty, append to the existing list + env_filter="$env_filter|/$scl" + done + fi + # Get environment variables from `docker run` + loop_envs=$(docker run --rm "$IMAGE_NAME" /bin/bash -c "env") + run_envs=$(docker run --rm "$IMAGE_NAME" /bin/bash -c "X_SCLS= scl enable $enabled_scls env") + # Check if the values are set twice in the second set of envs + ct_check_envs_set "$env_filter" "$run_envs" "$loop_envs" "*VALUE*VALUE*" || return 1 + echo " All scl_enable values present" + return 0 +} + +# 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" + # shellcheck source=/dev/null + [ -f "test/$test_case" ] && source "test/$test_case" + # shellcheck source=/dev/null + [ -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: +# /-cert-selfsigned.pem -- public PEM cert +# /-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 + 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 + 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=$(tail -c 3 "${response_file}") + if [ "${response_code}" -eq "${expected_code}" ]; then + result=0 + fi + grep -qP -e "${body_regexp}" "${response_file}" || 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 ] || [ "${attempt}" -gt "${ignore_error_attempts}" ] || [ "${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 + rhel*) + registry=registry.redhat.io + ;; + *) + registry=quay.io + ;; + esac + echo "$registry" +} + + # ct_get_public_image_name OS BASE_IMAGE_NAME VERSION +# ---------------- +# Transform the arguments into public image name +# Argument: OS - string containing the os version +# Argument: BASE_IMAGE_NAME - string containing the base name of the image as defined in the Makefile +# Argument: VERSION - string containing the version of the image as defined in the Makefile +ct_get_public_image_name() { + local os=$1; shift + local base_image_name=$1; shift + local version=$1; shift + + local public_image_name + local registry + + registry=$(ct_registry_from_os "$os") + if [ "$os" == "rhel7" ]; then + public_image_name=$registry/rhscl/$base_image_name-${version//./}-rhel7 + elif [ "$os" == "rhel8" ]; then + public_image_name=$registry/rhel8/$base_image_name-${version//./} + elif [ "$os" == "centos7" ]; then + public_image_name=$registry/centos7/$base_image_name-${version//./}-centos7 + elif [ "$os" == "centos8" ]; then + public_image_name=$registry/centos8/$base_image_name-${version//./}-centos8 + fi + + echo "$public_image_name" +} + +# 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= + local incremental=false + local mount_options=() + + # Run the entire thing inside a subshell so that we do not leak shell options outside of the function + ( + # Error out if any part of the build fails + set -e + + # Use /tmp to not pollute cwd + tmpdir=$(mktemp -d) + df_name=$(mktemp -p "$tmpdir" Dockerfile.XXXX) + cd "$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=$(docker inspect -f "{{.Config.User}}" "$src_image") + # Default to root if no user is set by the image + user=${user:-0} + # run the user through the image in case it is non-numeric or does not exist + if ! user_id=$(ct_get_uid_from_image "$user" "$src_image"); then + echo "Terminating s2i build." + return 1 + fi + + echo "$s2i_args" | grep -q "\-\-incremental" && incremental=true + if $incremental; then + inc_tmp=$(mktemp -d --tmpdir incremental.XXXX) + setfacl -m "u:$user_id:rwx" "$inc_tmp" + # Check if the image exists, build should fail (for testing use case) if it does not + docker images "$dst_image" &>/dev/null || (echo "Image $dst_image not found."; false) + # Run the original image with a mounted in volume and get the artifacts out of it + cmd="if [ -s /usr/libexec/s2i/save-artifacts ]; then /usr/libexec/s2i/save-artifacts > \"$inc_tmp/artifacts.tar\"; else touch \"$inc_tmp/artifacts.tar\"; fi" + docker run --rm -v "$inc_tmp:$inc_tmp:Z" "$dst_image" bash -c "$cmd" + # Move the created content into the $tmpdir for the build to pick it up + mv "$inc_tmp/artifacts.tar" "$tmpdir/" + fi + # 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 <"$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" + # Check if CA autority is present on host and add it into Dockerfile + [ -f "$(full_ca_file_path)" ] && echo "RUN cd /etc/pki/ca-trust/source/anchors && update-ca-trust extract" >>"$df_name" + + # Add in artifacts if doing an incremental build + if $incremental; then + { echo "RUN mkdir /tmp/artifacts" + echo "ADD artifacts.tar /tmp/artifacts" + echo "RUN chown -R $user_id:0 /tmp/artifacts" ; } >>"$df_name" + fi + + 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 + + # Check if -v parameter is present in s2i_args and add it into docker build command + read -ra mount_options <<< "$(echo "$s2i_args" | grep -o -e '\(-v\)[[:space:]]\.*\S*' || true)" + + # Run the build and tag the result + docker build ${mount_options[@]+"${mount_options[@]}"} -f "$df_name" --no-cache=true -t "$dst_image" . + ) +} + +# ct_s2i_multistage_build APP_PATH SRC_IMAGE DST_IMAGE SEC_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 process +# Argument: SEC_IMAGE - image to be used as the base for the result of the build process +# 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 environment variable definitions. +ct_s2i_multistage_build() { + + local app_path=$1; shift + local src_image=$1; shift + local sec_image=$1; shift + local dst_image=$1; shift + local s2i_args=$*; + local local_app="app-src" + local user_id= + local mount_options=() + + + # Run the entire thing inside a subshell so that we do not leak shell options outside of the function + ( + # Error out if any part of the build fails + set -e + + user=$(docker inspect -f "{{.Config.User}}" "$src_image") + # Default to root if no user is set by the image + user=${user:-0} + # run the user through the image in case it is non-numeric or does not exist + if ! user_id=$(ct_get_uid_from_image "$user" "$src_image"); then + echo "Terminating s2i build." + return 1 + fi + + # Use /tmp to not pollute cwd + tmpdir=$(mktemp -d) + df_name=$(mktemp -p "$tmpdir" Dockerfile.XXXX) + cd "$tmpdir" + + # If the path exists on the local host, copy it into the directory for the build + # Otherwise handle it as a link to a git repository + if [ -e "${app_path/file:\/\//}/." ] ; then + mkdir -p "$local_app" + # Strip file:// from APP_PATH and copy its contents into current context + cp -r "${app_path/file:\/\//}/." "$local_app" + + else + ct_clone_git_repository "$app_path" "$local_app" + fi + + cat <"$df_name" +# First stage builds the application +FROM $src_image as builder +# Add application sources to a directory that the assemble script expects them +# and set permissions so that the container runs without root access +USER 0 +ADD app-src /tmp/src +RUN chown -R 1001:0 /tmp/src +$(echo "$s2i_args" | grep -o -e '\(-e\|--env\)[[:space:]=]\S*=\S*' | sed -e 's/-e /ENV /' -e 's/--env[ =]/ENV /') +# Check if CA autority is present on host and add it into Dockerfile +$([ -f "$(full_ca_file_path)" ] && echo "RUN cd /etc/pki/ca-trust/source/anchors && update-ca-trust extract") +USER $user_id +# Install the dependencies +RUN /usr/libexec/s2i/assemble +# Second stage copies the application to the minimal image +FROM $sec_image +# Copy the application source and build artifacts from the builder image to this one +COPY --from=builder \$HOME \$HOME +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +EOF + + # Check if -v parameter is present in s2i_args and add it into docker build command + read -ra mount_options <<< "$(echo "$s2i_args" | grep -o -e '\(-v\)[[:space:]]\.*\S*' || true)" + + docker build ${mount_options[@]+"${mount_options[@]}"} -f "$df_name" --no-cache=true -t "$dst_image" . + ) +} + +# ct_check_image_availability PUBLIC_IMAGE_NAME +# ---------------------------- +# Pull an image from the public repositories to see if the image is already available. +# Argument: PUBLIC_IMAGE_NAME - string containing the public name of the image to pull +ct_check_image_availability() { + local public_image_name=$1; + + # Try pulling the image to see if it is accessible + if ! ct_pull_image "$public_image_name" &>/dev/null; then + echo "$public_image_name could not be downloaded via 'docker'" + return 1 + fi +} + +# ct_check_latest_imagestreams +# ----------------------------- +# Check if the latest version present in Makefile in the variable VERSIONS +# is present in all imagestreams. +# Also the latest tag in the imagestreams has to contain the latest version +ct_check_latest_imagestreams() { + local latest_version= + local test_lib_dir= + + # We only maintain imagestreams for RHEL and CentOS (Community) + if [[ "$OS" =~ ^fedora.* ]] ; then + echo "Imagestreams for Fedora are not maintained, skipping ct_check_latest_imagestreams" + return 0 + fi + + # Check only lines which starts with VERSIONS + latest_version=$(grep '^VERSIONS' Makefile | rev | cut -d ' ' -f 1 | rev ) + # Fall back to previous version if the latest is excluded for this OS + [ -f "$latest_version/.exclude-$OS" ] && latest_version=$(grep '^VERSIONS' Makefile | rev | cut -d ' ' -f 2 | rev ) + # Only test the imagestream once, when the version matches + # ignore the SC warning, $VERSION is always available + # shellcheck disable=SC2153 + if [ "$latest_version" == "$VERSION" ]; then + test_lib_dir=$(dirname "$(readlink -f "$0")") + python3 "${test_lib_dir}/check_imagestreams.py" "$latest_version" + else + echo "Image version $VERSION is not latest, skipping ct_check_latest_imagestreams" + fi +} + +# ct_show_resources +# ---------------- +# Prints the available resources +ct_show_resources() +{ + echo "Resources info:" + echo "Memory:" + free -h + echo "Storage:" + df -h || : + echo "CPU" + lscpu +} + +# ct_clone_git_repository +# ----------------------------- +# Argument: app_url - git URI pointing to a repository, supports "@" to indicate a different branch +# Argument: app_dir (optional) - name of the directory to clone the repository into +ct_clone_git_repository() +{ + local app_url=$1; shift + local app_dir=$1 + + # If app_url contains @, the string after @ is considered + # as a name of a branch to clone instead of the main/master branch + IFS='@' read -ra git_url_parts <<< "${app_url}" + + if [ -n "${git_url_parts[1]}" ]; then + git_clone_cmd="git clone --branch ${git_url_parts[1]} ${git_url_parts[0]} ${app_dir}" + else + git_clone_cmd="git clone ${app_url} ${app_dir}" + fi + + if ! $git_clone_cmd ; then + echo "ERROR: Git repository ${app_url} cannot be cloned into ${app_dir}." + return 1 + fi +} + +# ct_get_uid_from_image +# ----------------------------- +# Argument: user - user to get uid for inside the image +# Argument: src_image - image to use for user information +ct_get_uid_from_image() +{ + local user=$1; shift + local src_image=$1 + local user_id= + + # NOTE: The '-eq' test is used to check if $user is numeric as it will fail if $user is not an integer + if ! [ "$user" -eq "$user" ] 2>/dev/null && ! user_id=$(docker run --rm "$src_image" bash -c "id -u $user 2>/dev/null"); then + echo "ERROR: id of user $user not found inside image $src_image." + return 1 + else + echo "${user_id:-$user}" + fi +} + +# ct_test_app_dockerfile +# ----------------------------- +# Argument: dockerfile - path to a Dockerfile that will be used for building an image +# (must work with an application directory called 'app-src') +# Argument: app_url - git or local URI with a testing application, supports "@" to indicate a different branch +# Argument: body_regexp - PCRE regular expression that must match the response body +# Argument: app_dir - name of the application directory that is used in the Dockerfile +# Argument: port - Optional port number (default: 8080) +ct_test_app_dockerfile() { + local dockerfile=$1 + local app_url=$2 + local expected_text=$3 + local app_dir=$4 # this is a directory that must match with the name in the Dockerfile + local port=${5:-8080} + local app_image_name=myapp + local ret + local cname=app_dockerfile + + if [ -z "$app_dir" ] ; then + echo "ERROR: Option app_dir not set. Terminating the Dockerfile build." + return 1 + fi + + if ! [ -r "${dockerfile}" ] || ! [ -s "${dockerfile}" ] ; then + echo "ERROR: Dockerfile ${dockerfile} does not exist or is empty." + echo "Terminating the Dockerfile build." + return 1 + fi + + CID_FILE_DIR=${CID_FILE_DIR:-$(mktemp -d)} + local dockerfile_abs + dockerfile_abs=$(readlink -f "${dockerfile}") + tmpdir=$(mktemp -d) + pushd "$tmpdir" >/dev/null + cp "${dockerfile_abs}" Dockerfile + + # Rewrite the source image to what we test + sed -i -e "s|^FROM.*$|FROM $IMAGE_NAME|" Dockerfile + # a bit more verbose, but should help debugging failures + echo "Using this Dockerfile:" + cat Dockerfile + + if [ -d "$app_url" ] ; then + echo "Copying local folder: $app_url -> $app_dir." + cp -Lr "$app_url" "$app_dir" + else + if ! ct_clone_git_repository "$app_url" "$app_dir" ; then + echo "Terminating the Dockerfile build." + return 1 + fi + fi + + echo "Building '${app_image_name}' image using docker build" + if ! docker build --no-cache=true -t "${app_image_name}" . ; then + echo "ERROR: The image cannot be built from ${dockerfile} and application ${app_url}." + echo "Terminating the Dockerfile build." + return 1 + fi + + if ! docker run -d --cidfile="${CID_FILE_DIR}/app_dockerfile" --rm "${app_image_name}" ; then + echo "ERROR: The image ${app_image_name} cannot be run for ${dockerfile} and application ${app_url}." + echo "Terminating the Dockerfile build." + return 1 + fi + echo "Waiting for ${app_image_name} to start" + ct_wait_for_cid "${CID_FILE_DIR}/app_dockerfile" + + ip="$(ct_get_cip "${cname}")" + ct_test_response "http://$ip:${port}" 200 "${expected_text}" + ret=$? + + # cleanup + docker kill "$(ct_get_cid "${cname}")" + sleep 2 + docker rmi "${app_image_name}" + popd >/dev/null + rm -rf "${tmpdir}" + rm -f "${CID_FILE_DIR}/${cname}" + return $ret +} + +# vim: set tabstop=2:shiftwidth=2:expandtab: diff --git a/test/virtualenv-uwsgi-test-app/app.sh b/test/virtualenv-uwsgi-test-app/app.sh deleted file mode 100755 index 397240a..0000000 --- a/test/virtualenv-uwsgi-test-app/app.sh +++ /dev/null @@ -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