diff --git a/Dockerfile b/Dockerfile index 1571e8b..9b7dcd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,11 +46,17 @@ RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapp 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/ / +# 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. @@ -58,6 +64,17 @@ COPY ./root/ / # 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 diff --git a/README.md b/README.md index 425d199..2491fae 100644 --- a/README.md +++ b/README.md @@ -260,12 +260,18 @@ file inside your source code repository. 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 and related python packages (setuptools and wheel) be upgraded to the most recent version - before any Python packages are installed. If not set it will use whatever - the default version is included by the platform for the Python version being used. + 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** @@ -323,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 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/s2i/bin/assemble b/s2i/bin/assemble index 10996be..3d36bdd 100755 --- a/s2i/bin/assemble +++ b/s2i/bin/assemble @@ -53,20 +53,11 @@ mv /tmp/src/* "$HOME" # set permissions for any installed artifacts fix-permissions /opt/app-root -P -# We have to first upgrade pip to at least 19.3 because: -# * pip < 9 does not support different packages' versions for Python 2/3 -# * pip < 19.3 does not support manylinux2014 wheels. Only manylinux2014 wheels -# support platforms like ppc64le, aarch64 or armv7 -echo "---> Upgrading pip to version 19.3.1 ..." -if ! pip install -U "pip==19.3.1"; then - echo "WARNING: Installation of 'pip==19.3.1' failed, trying again from official PyPI with pip --isolated install" - pip install --isolated -U "pip==19.3.1" -fi if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then - echo "---> Upgrading pip to latest version ..." + 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" + 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 diff --git a/s2i/bin/run b/s2i/bin/run index b5c2441..21a38f4 100755 --- a/s2i/bin/run +++ b/s2i/bin/run @@ -93,6 +93,11 @@ if should_migrate; then fi fi +# If not set, use 8080 as the default port +if [ -z "$PORT" ]; then + PORT=8080 +fi + 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 @@ -105,17 +110,27 @@ if is_gunicorn_installed; then 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." - maybe_run_in_init_wrapper 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/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/django-different-port-test-app/project/__init__.py b/test/django-different-port-test-app/project/__init__.py new file mode 100644 index 0000000..e69de29 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..5827794 --- /dev/null +++ b/test/django-different-port-test-app/project/settings.py @@ -0,0 +1,104 @@ +""" +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/ +""" + +# 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', +) + +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', +) + +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..b5e472c --- /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/', include(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..30eb621 --- /dev/null +++ b/test/django-different-port-test-app/requirements.txt @@ -0,0 +1 @@ +Django==1.11.29 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/run b/test/run index d14e9a7..291c3df 100755 --- a/test/run +++ b/test/run @@ -6,7 +6,7 @@ # IMAGE_NAME specifies a name of the candidate image used for testing. # The image has to be available before this script is executed. # -declare -a WEB_APPS=({standalone,setup,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) +declare -a WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,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]}"))" @@ -37,9 +37,6 @@ IMAGE_NAME=${IMAGE_NAME:-centos/python-${VERSION//./}-centos7} . test/test-lib.sh -# TODO: This should be part of the image metadata -test_port=8080 - info() { echo -e "\n\e[1m[INFO] $@\e[0m\n" } @@ -294,6 +291,13 @@ for app in ${@:-${WEB_APPS[@]}}; do # 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=$?