diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..39459aba --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +--- + +version: 2 +updates: + - package-ecosystem: gitsubmodule + directory: / + schedule: + interval: weekly + labels: + - 'ready for review' + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - 'ready for review' diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml new file mode 100644 index 00000000..7cdbfca4 --- /dev/null +++ b/.github/workflows/build-and-push.yml @@ -0,0 +1,158 @@ +name: Build and push container images to Quay.io registry +on: + push: + branches: + - master + schedule: + - cron: '0 1 * * 3' + +jobs: + build-and-push: + if: github.repository_owner == 'sclorg' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - dockerfile: "3.9/Dockerfile.c9s" + docker_context: "3.9" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-39-c9s" + + - dockerfile: "3.9-minimal/Dockerfile.c9s" + docker_context: "3.9-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-39-minimal-c9s" + + - dockerfile: "3.12-minimal/Dockerfile.rhel8" + docker_context: "3.12-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "el8" + image_name: "python-312-minimal-el8" + + - dockerfile: "3.12-minimal/Dockerfile.c9s" + docker_context: "3.12-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-312-minimal-c9s" + + - dockerfile: "3.12-minimal/Dockerfile.c10s" + docker_context: "3.12-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c10s" + image_name: "python-312-minimal-c10s" + + - dockerfile: "3.12/Dockerfile.c9s" + docker_context: "3.12" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-312-c9s" + + - dockerfile: "3.12/Dockerfile.c10s" + docker_context: "3.12" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c10s" + image_name: "python-312-c10s" + + - dockerfile: "3.13/Dockerfile.fedora" + docker_context: "3.13" + registry_namespace: "fedora" + quayio_username: "QUAY_IMAGE_FEDORA_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_FEDORA_BUILDER_TOKEN" + tag: "313" + image_name: "python-313" + + - dockerfile: "3.13-minimal/Dockerfile.fedora" + docker_context: "3.13-minimal" + registry_namespace: "fedora" + quayio_username: "QUAY_IMAGE_FEDORA_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_FEDORA_BUILDER_TOKEN" + tag: "313" + image_name: "python-313-minimal" + + - dockerfile: "3.13/Dockerfile.c10s" + docker_context: "3.13" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c10s" + image_name: "python-313-c10s" + + - dockerfile: "3.13-minimal/Dockerfile.c10s" + docker_context: "3.13-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c10s" + image_name: "python-313-minimal-c10s" + + - dockerfile: "3.14/Dockerfile.fedora" + docker_context: "3.14" + registry_namespace: "fedora" + quayio_username: "QUAY_IMAGE_FEDORA_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_FEDORA_BUILDER_TOKEN" + tag: "314" + image_name: "python-314" + + - dockerfile: "3.14-minimal/Dockerfile.fedora" + docker_context: "3.14-minimal" + registry_namespace: "fedora" + quayio_username: "QUAY_IMAGE_FEDORA_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_FEDORA_BUILDER_TOKEN" + tag: "314" + image_name: "python-314-minimal" + + - dockerfile: "3.14/Dockerfile.c9s" + docker_context: "3.14" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-314-c9s" + + - dockerfile: "3.14-minimal/Dockerfile.c9s" + docker_context: "3.14-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c9s" + image_name: "python-314-minimal-c9s" + + - dockerfile: "3.14-minimal/Dockerfile.c10s" + docker_context: "3.14-minimal" + registry_namespace: "sclorg" + quayio_username: "QUAY_IMAGE_SCLORG_BUILDER_USERNAME" + quayio_token: "QUAY_IMAGE_SCLORG_BUILDER_TOKEN" + tag: "c10s" + image_name: "python-314-minimal-c10s" + + steps: + - name: Build and push to quay.io registry + uses: sclorg/build-and-push-action@v4 + with: + registry: "quay.io" + registry_namespace: ${{ matrix.registry_namespace }} + registry_username: ${{ secrets[matrix.quayio_username] }} + registry_token: ${{ secrets[matrix.quayio_token] }} + dockerfile: ${{ matrix.dockerfile }} + tag: ${{ matrix.tag }} + image_name: ${{ matrix.image_name }} + archs: amd64, arm64, ppc64le + readme: "${{ matrix.docker_context }}/README.md" + quay_application_token: ${{ secrets.QUAY_IMAGE_SCLORG_UPDATE_DESC }} diff --git a/.github/workflows/container-tests.yml b/.github/workflows/container-tests.yml new file mode 100644 index 00000000..67904ab7 --- /dev/null +++ b/.github/workflows/container-tests.yml @@ -0,0 +1,19 @@ +--- + +on: + issue_comment: + types: + - created +jobs: + distgen-check: + uses: "sclorg/ci-actions/.github/workflows/distgen-check.yml@main" + check-readme: + uses: "sclorg/ci-actions/.github/workflows/check-readme.yml@main" + container-tests: + needs: check-readme + uses: "sclorg/ci-actions/.github/workflows/container-tests.yml@main" + with: + enabled-tests: '["container","container-pytest","openshift-pytest"]' + versions: '[ "3.9", "3.9-minimal", "3.12", "3.12-minimal", "3.13", "3.13-minimal", "3.14", "3.14-minimal" ]' + openshift-versions: '[ "3.8", "3.9", "3.12", "3.12-minimal" ]' + secrets: inherit diff --git a/.gitignore b/.gitignore index 64b6b812..65c7ac03 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,15 @@ staticfiles/ openshift.local.* +# Generated scripts / data +auto_targets.mk +.image-id +.image-id.raw +.image-id-from + +# help.1 files complied by container-common-scripts from README.md files +*/root/help.1 + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..aee9c814 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "common"] + path = common + url = https://github.com/sclorg/container-common-scripts.git diff --git a/2.7/Dockerfile b/2.7/Dockerfile deleted file mode 100644 index 3869d22d..00000000 --- a/2.7/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# This image provides a Python 2.7 environment you can use to run your Python -# applications. -FROM openshift/base-centos7 - -MAINTAINER SoftwareCollections.org - -EXPOSE 8080 - -ENV PYTHON_VERSION=2.7 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 2.7 applications" \ - io.k8s.display-name="Python 2.7" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python27,rh-python27" - -RUN yum install -y centos-release-scl && \ - INSTALL_PKGS="libjpeg-turbo libjpeg-turbo-devel python27 python27-python-devel python27-python-setuptools python27-python-pip epel-release nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs --enablerepo=centosplus $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove centos-logos (httpd dependency, ~20MB of graphics) to keep image - # size smaller. - rpm -e --nodeps centos-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# In order to drop the root user, we have to make some directories world -# writable as OpenShift default security model is to run the container under -# random UID. -RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/2.7/Dockerfile.rhel7 b/2.7/Dockerfile.rhel7 deleted file mode 100644 index 646054e5..00000000 --- a/2.7/Dockerfile.rhel7 +++ /dev/null @@ -1,47 +0,0 @@ -# This image provides a Python 2.7 environment you can use to run your Python -# applications. -FROM openshift/base-rhel7 - -EXPOSE 8080 - -ENV PYTHON_VERSION=2.7 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 2.7 applications" \ - io.k8s.display-name="Python 2.7" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python27,rh-python27" \ - # Labels consumed by Red Hat build service. - BZComponent="python27-docker" \ - Name="rhscl/python-27-rhel7" \ - Version="2.7" \ - Release="1" \ - Architecture="x86_64" - -RUN yum-config-manager --enable rhel-server-rhscl-7-rpms && \ - yum-config-manager --enable rhel-7-server-optional-rpms && \ - yum-config-manager --enable rhel-7-server-ose-3.0-rpms && \ - yum-config-manager --disable epel >/dev/null || : && \ - INSTALL_PKGS="python27 python27-python-devel python27-python-setuptools python27-python-pip nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove redhat-logos (httpd dependency) to keep image size smaller. - rpm -e --nodeps redhat-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# In order to drop the root user, we have to make some directories world -# writable as OpenShift default security model is to run the container under -# random UID. -RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/2.7/README.md b/2.7/README.md deleted file mode 100644 index 9d4a559a..00000000 --- a/2.7/README.md +++ /dev/null @@ -1,205 +0,0 @@ -Python Docker image -=================== - -This repository contains the source for building various versions of -the Python application as a reproducible Docker image using -[source-to-image](https://github.com/openshift/source-to-image). -Users can choose between RHEL and CentOS based builder images. -The resulting image can be run using [Docker](http://docker.io). - - -Usage ---------------------- -To build a simple [python-sample-app](https://github.com/openshift/sti-python/tree/master/2.7/test/setup-test-app) application -using standalone [S2I](https://github.com/openshift/source-to-image) and then run the -resulting image with [Docker](http://docker.io) execute: - -* **For RHEL based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=2.7/test/setup-test-app/ rhscl/python-27-rhel7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -* **For CentOS based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=2.7/test/setup-test-app/ centos/python-27-centos7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -**Accessing the application:** -``` -$ curl 127.0.0.1:8080 -``` - - -Repository organization ------------------------- -* **``** - - * **Dockerfile** - - CentOS based Dockerfile. - - * **Dockerfile.rhel7** - - RHEL based Dockerfile. In order to perform build or test actions on this - Dockerfile you need to run the action on a properly subscribed RHEL machine. - - * **`s2i/bin/`** - - This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): - - * **assemble** - - Used to install the sources into the location where the application - will be run and prepare the application for deployment (eg. installing - dependencies, etc.) - - * **run** - - This script is responsible for running the application by using the - application web server. - - * **usage*** - - This script prints the usage of this image. - - * **`contrib/`** - - This folder contains a file with commonly used modules. - - * **`test/`** - - This folder contains a [S2I](https://github.com/openshift/source-to-image) - test framework with a simple server. - - * **`setup-test-app/`** - - Simple Gunicorn application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **`standalone-test-app/`** - - Simple standalone application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **run** - - Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. - - -Environment variables ---------------------- - -To set these environment variables, you can place them as a key value pair into a `.sti/environment` -file inside your source code repository. - -* **APP_FILE** - - Used to run the application from a Python script. - This should be a path to a Python file (defaults to `app.py`) that will be - passed to the Python interpreter to start the application. - -* **APP_MODULE** - - Used to run the application with Gunicorn, as documented - [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). - This variable specifies a WSGI callable with the pattern - `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path - of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the - specified module. - Gunicorn will look for a WSGI callable named `application` if not specified. - - If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` - file in your project and use it if it exists. - - If using `setup.py` for installing the application, the `MODULE_NAME` part - can be read from there. For an example, see - [setup-test-app](https://github.com/openshift/sti-python/tree/master/2.7/test/setup-test-app). - -* **APP_CONFIG** - - Path to a valid Python file with a - [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. - -* **DISABLE_COLLECTSTATIC** - - Set this variable to a non-empty value to inhibit the execution of - 'manage.py collectstatic' during the build. This only affects Django projects. - -* **DISABLE_MIGRATE** - - Set this variable to a non-empty value to inhibit the execution of 'manage.py migrate' - when the produced image is run. This only affects Django projects. - -Source repository layout ------------------------- - -You do not need to change anything in your existing Python project's repository. -However, if these files exist they will affect the behavior of the build process: - -* **requirements.txt** - - List of dependencies to be installed with `pip`. The format is documented - [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). - - -* **setup.py** - - Configures various aspects of the project, including installation of - dependencies, as documented - [here](https://packaging.python.org/en/latest/distributing.html#setup-py). - For most projects, it is sufficient to simply use `requirements.txt`. - - -Run strategies --------------- - -The Docker image produced by sti-python executes your project in one of the -following ways, in precedence order: - -* **Gunicorn** - - The Gunicorn WSGI HTTP server is used to serve your application in the case that it - is installed. It can be installed by listing it either in the `requirements.txt` - file or in the `install_requires` section of the `setup.py` file. - - If a file named `wsgi.py` is present in your repository, it will be used as - the entry point to your application. This can be overridden with the - environment variable `APP_MODULE`. - This file is present in Django projects by default. - - If you have both Django and Gunicorn in your requirements, your Django project - will automatically be served using Gunicorn. - -* **Django development server** - - If you have Django in your requirements but don't have Gunicorn, then your - application will be served using Django's development web server. However, this is not - recommended for production environments. - -* **Python script** - - This is the most general way of executing your application. It will be used - in the case where you specify a path to a Python script via the `APP_FILE` environment - variable, defaulting to a file named `app.py` if it exists. The script is - passed to a regular Python interpreter to launch your application. - -Hot deploy ---------------------- - -If you are using Django, hot deploy will work out of the box. - -To enable hot deploy while using Gunicorn, make sure you have a Gunicorn -configuration file inside your repository with the -[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) -option set to `true`. Make sure to specify your config via the `APP_CONFIG` -environment variable. - -To change your source code in running container, use Docker's -[exec](https://docs.docker.com/reference/commandline/exec/) command: - -``` -docker exec -it /bin/bash -``` - -After you enter into the running container, your current directory is set -to `/opt/app-root/src`, where the source code is located. diff --git a/2.7/contrib/etc/generate_container_user b/2.7/contrib/etc/generate_container_user deleted file mode 100644 index 229b986a..00000000 --- a/2.7/contrib/etc/generate_container_user +++ /dev/null @@ -1,9 +0,0 @@ -# Set current user in nss_wrapper -PASSWD_DIR="/opt/app-root/etc" - -export USER_ID=$(id -u) -export GROUP_ID=$(id -g) -envsubst < ${PASSWD_DIR}/passwd.template > ${PASSWD_DIR}/passwd -export LD_PRELOAD=libnss_wrapper.so -export NSS_WRAPPER_PASSWD=${PASSWD_DIR}/passwd -export NSS_WRAPPER_GROUP=/etc/group diff --git a/2.7/contrib/etc/passwd.template b/2.7/contrib/etc/passwd.template deleted file mode 100644 index af0814d9..00000000 --- a/2.7/contrib/etc/passwd.template +++ /dev/null @@ -1,14 +0,0 @@ -root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -operator:x:11:0:operator:/root:/sbin/nologin -games:x:12:100:games:/usr/games:/sbin/nologin -ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin -nobody:x:99:99:Nobody:/:/sbin/nologin -default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/sbin/nologin diff --git a/2.7/contrib/etc/scl_enable b/2.7/contrib/etc/scl_enable deleted file mode 100644 index 8b8fed50..00000000 --- a/2.7/contrib/etc/scl_enable +++ /dev/null @@ -1,5 +0,0 @@ -# IMPORTANT: Do not add more content to this file unless you know what you are -# 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 python27 diff --git a/2.7/s2i/bin/assemble b/2.7/s2i/bin/assemble deleted file mode 100755 index 60ae7ff1..00000000 --- a/2.7/s2i/bin/assemble +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_collectstatic() { - is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] -} - -set -e - -echo "---> Copying application source ..." -cp -Rf /tmp/src/. ./ - -if [[ -f requirements.txt ]]; then - echo "---> Installing dependencies ..." - pip install --user -r requirements.txt -fi - -if [[ -f setup.py ]]; then - echo "---> Installing application ..." - python setup.py develop --user -fi - -if should_collectstatic; then - ( - echo "---> Collecting Django static files ..." - - # Find shallowest manage.py script, either ./manage.py or /manage.py - manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - - if [[ ! -f "$manage_file" ]]; then - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "'manage.py collectstatic' ignored." - exit - fi - - if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then - echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" - echo " $ python $manage_file collectstatic --noinput" - echo "Ignore this warning if you're not serving static files with Django." - exit - fi - - python $manage_file collectstatic --noinput - ) -fi - -# Fix source directory permissions -fix-permissions ./ -# set permissions for any installed artifacts -fix-permissions /opt/app-root - -# remove pip temporary directory -rm -rf /tmp/pip_build_default diff --git a/2.7/s2i/bin/run b/2.7/s2i/bin/run deleted file mode 100755 index 1c03566c..00000000 --- a/2.7/s2i/bin/run +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -source /opt/app-root/etc/generate_container_user - -function is_gunicorn_installed() { - hash gunicorn &>/dev/null -} - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_migrate() { - is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] -} - -set -e - -APP_FILE="${APP_FILE:-app.py}" -if [[ -f "$APP_FILE" ]]; then - echo "---> Running application from Python script ($APP_FILE) ..." - exec python "$APP_FILE" -fi -echo "WARNING: file '$APP_FILE' not found." - -# Find shallowest manage.py script, either ./manage.py or /manage.py -manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - -if should_migrate; then - if [[ -f "$manage_file" ]]; then - echo "---> Migrating database ..." - python "$manage_file" migrate --noinput - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py migrate'." - fi -fi - -if is_gunicorn_installed; then - if [[ -z "$APP_MODULE" ]]; then - # Find shallowest wsgi.py file, one of ./wsgi.py, /wsgi.py or //wsgi.py, - # replace "/" with "." and remove ".py" suffix - APP_MODULE=$(find . -maxdepth 3 -type f -name 'wsgi.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1 | sed 's:/:.:;s:.py$::') - fi - - if [[ -z "$APP_MODULE" && -f setup.py ]]; then - APP_MODULE="$(python setup.py --name)" - fi - - if [[ "$APP_MODULE" ]]; then - echo "---> Serving application with gunicorn ($APP_MODULE) ..." - exec gunicorn "$APP_MODULE" --bind=0.0.0.0:8080 --access-logfile=- --config "$APP_CONFIG" - fi -fi - -if is_django_installed; then - if [[ -f "$manage_file" ]]; then - echo "---> Serving application with 'manage.py runserver' ..." - echo "WARNING: this is NOT a recommended way to run you application in production!" - echo "Consider using gunicorn or some other production web server." - exec python "$manage_file" runserver 0.0.0.0:8080 - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py runserver'." - fi -fi - ->&2 echo "ERROR: don't know how to run your application." ->&2 echo "Please set either APP_MODULE or APP_FILE environment variables, or create a file 'app.py' to launch your application." -exit 1 diff --git a/2.7/s2i/bin/usage b/2.7/s2i/bin/usage deleted file mode 100755 index d752e180..00000000 --- a/2.7/s2i/bin/usage +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` -NAMESPACE=centos -[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl - -cat </dev/null -} - -container_exists() { - image_exists $(cat $cid_file) -} - -container_ip() { - docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) -} - -run_s2i_build() { - info "Building the ${1} application image ..." - s2i build ${s2i_args} file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp -} - -prepare() { - if ! image_exists ${IMAGE_NAME}; then - echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." - exit 1 - fi - # TODO: S2I build require the application is a valid 'GIT' repository, we - # should remove this restriction in the future when a file:// is used. - info "Preparing to test ${1} ..." - pushd ${test_dir}/${1} >/dev/null - git init - git config user.email "build@localhost" && git config user.name "builder" - git add -A && git commit -m "Sample commit" - popd >/dev/null -} - -run_test_application() { - docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} -p ${test_port} ${IMAGE_NAME}-testapp -} - -cleanup_app() { - info "Cleaning up app container ..." - if [ -f $cid_file ]; then - if container_exists; then - docker stop $(cat $cid_file) - fi - fi -} - -cleanup() { - info "Cleaning up the test application image" - if image_exists ${IMAGE_NAME}-testapp; then - docker rmi -f ${IMAGE_NAME}-testapp - fi - rm -rf ${test_dir}/${1}/.git -} - -check_result() { - local result="$1" - if [[ "$result" != "0" ]]; then - info "TEST FAILED (${result})" - cleanup - exit $result - fi -} - -wait_for_cid() { - local max_attempts=10 - local sleep_time=1 - local attempt=1 - local result=1 - info "Waiting for application container to start $CONTAINER_ARGS ..." - while [ $attempt -le $max_attempts ]; do - [ -f $cid_file ] && [ -s $cid_file ] && break - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done -} - -test_s2i_usage() { - info "Testing 's2i usage' ..." - s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null -} - -test_docker_run_usage() { - info "Testing 'docker run' usage ..." - docker run ${IMAGE_NAME} &>/dev/null -} - -test_scl_usage() { - local run_cmd="$1" - local expected="$2" - local cid_file="$3" - - info "Testing the image SCL enable" - out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - # out=$(docker run --rm -i ${IMAGE_NAME} /bin/bash -i <<< "${run_cmd}" 2>&1) - # if ! echo "${out}" | grep -q "${expected}"; then - # echo "ERROR[-i /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - # return 1 - # fi - # out=$(docker run --rm -i ${IMAGE_NAME} /bin/bash -l <<< "${run_cmd}" 2>&1) - # if ! echo "${out}" | grep -q "${expected}"; then - # echo "ERROR[-i /bin/bash -l "${run_cmd}"] Expected '${expected}', got '${out}'" - # return 1 - # fi - # out=$(docker run --rm -i ${IMAGE_NAME} /bin/sh -ic "${run_cmd}" 2>&1) - # if ! echo "${out}" | grep -q "${expected}"; then - # echo "ERROR[/bin/sh -c "${run_cmd}"] Expected '${expected}', got '${out}'" - # return 1 - # fi - out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi -} - -test_connection() { - info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." - local max_attempts=30 - local sleep_time=1 - local attempt=1 - local result=1 - while [ $attempt -le $max_attempts ]; do - response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) - status=$? - if [ $status -eq 0 ]; then - if [ $response_code -eq 200 ]; then - result=0 - fi - break - fi - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done - return $result -} - -test_application() { - local cid_file=$(mktemp -u --suffix=.cid) - # Verify that the HTTP connection can be established to test application container - run_test_application & - - # Wait for the container to write it's CID file - wait_for_cid - - test_scl_usage "python --version" "Python 2.7.8" "${cid_file}" - check_result $? - test_connection - check_result $? - cleanup_app -} - - -# Since we built the candidate image locally, we don't want S2I attempt to pull -# it from Docker hub -s2i_args="--force-pull=false" - -# Verify the 'usage' script is working properly when running the base image with 's2i usage ...' -test_s2i_usage -check_result $? - -# Verify the 'usage' script is working properly when running the base image with 'docker run ...' -test_docker_run_usage -check_result $? - -for app in ${WEB_APPS[@]}; do - prepare ${app} - run_s2i_build ${app} - check_result $? - - # test application with default user - test_application - - # test application with random user - CONTAINER_ARGS="-u 12345" test_application - - info "All tests for the ${app} finished successfully." - cleanup ${app} -done - -info "All tests finished successfully." diff --git a/2.7/test/standalone-test-app/app.py b/2.7/test/standalone-test-app/app.py deleted file mode 100644 index d786ee79..00000000 --- a/2.7/test/standalone-test-app/app.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -from gunicorn.app.base import BaseApplication -from gunicorn.six import iteritems - - -def wsgi_handler(environ, start_response): - start_response('200 OK', [('Content-Type','text/html')]) - return [b"Hello World from standalone WSGI application!"] - -class StandaloneApplication(BaseApplication): - def __init__(self, app, options=None): - self.options = options or {} - self.application = app - super(StandaloneApplication, self).__init__() - - def load_config(self): - config = dict([(key, value) for key, value in iteritems(self.options) - if key in self.cfg.settings and value is not None]) - for key, value in iteritems(config): - self.cfg.set(key.lower(), value) - - def load(self): - return self.application - -if __name__ == '__main__': - StandaloneApplication(wsgi_handler, {'bind': ':8080'}).run() diff --git a/2.7/test/standalone-test-app/requirements.txt b/2.7/test/standalone-test-app/requirements.txt deleted file mode 100644 index 8f22dccf..00000000 --- a/2.7/test/standalone-test-app/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gunicorn diff --git a/3.12-minimal/Dockerfile.c10s b/3.12-minimal/Dockerfile.c10s new file mode 100644 index 00000000..d66575c8 --- /dev/null +++ b/3.12-minimal/Dockerfile.c10s @@ -0,0 +1,104 @@ +FROM quay.io/centos/centos:stream10-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi10-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el10" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="sclorg/python-312-minimal-c10s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12-minimal/test/setup-test-app/ ubi10/python-312-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi10-python-312-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.12 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12-minimal/root/ / + +# Python 3.7+ 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.12-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.12 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12-minimal/Dockerfile.c9s b/3.12-minimal/Dockerfile.c9s new file mode 100644 index 00000000..bb4cd2ae --- /dev/null +++ b/3.12-minimal/Dockerfile.c9s @@ -0,0 +1,104 @@ +FROM quay.io/centos/centos:stream9-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el9" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="sclorg/python-312-minimal-c9s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12-minimal/test/setup-test-app/ ubi9/python-312-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-312-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.12 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12-minimal/root/ / + +# Python 3.7+ 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.12-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.12 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12-minimal/Dockerfile.rhel10 b/3.12-minimal/Dockerfile.rhel10 new file mode 100644 index 00000000..d133a9d4 --- /dev/null +++ b/3.12-minimal/Dockerfile.rhel10 @@ -0,0 +1,104 @@ +FROM ubi10-minimal:latest + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi10-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el10" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="ubi10/python-312-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12-minimal/test/setup-test-app/ ubi10/python-312-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi10-python-312-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.12 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12-minimal/root/ / + +# Python 3.7+ 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.12-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.12 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12-minimal/Dockerfile.rhel8 b/3.12-minimal/Dockerfile.rhel8 new file mode 100644 index 00000000..521afbb9 --- /dev/null +++ b/3.12-minimal/Dockerfile.rhel8 @@ -0,0 +1,104 @@ +FROM ubi8/ubi-minimal:latest + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi8-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el8" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="ubi8/python-312-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12-minimal/test/setup-test-app/ ubi8/python-312-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi8-python-312-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.12 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12-minimal/root/ / + +# Python 3.7+ 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.12-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.12 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12-minimal/Dockerfile.rhel9 b/3.12-minimal/Dockerfile.rhel9 new file mode 100644 index 00000000..86b7165f --- /dev/null +++ b/3.12-minimal/Dockerfile.rhel9 @@ -0,0 +1,104 @@ +FROM ubi9/ubi-minimal:latest + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el9" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="ubi9/python-312-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12-minimal/test/setup-test-app/ ubi9/python-312-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-312-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.12 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12-minimal/root/ / + +# Python 3.7+ 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.12-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.12 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12-minimal/README.md b/3.12-minimal/README.md new file mode 100644 index 00000000..ef094dbc --- /dev/null +++ b/3.12-minimal/README.md @@ -0,0 +1,125 @@ +Python 3.12 container image - minimal version +============================================ + +This container image is a special version of the [full Python 3.12 container image](https://github.com/sclorg/s2i-python-container/tree/master/3.12) +provided as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.12 applications. + +Because the minimal and full images work similarly, we document here only the differences and limitations +of the minimal container image. For the documentation of common features see the [full container image docs](https://github.com/sclorg/s2i-python-container/tree/master/3.12). + +The Python 3.12 minimal container image is currently considered a tech-preview and only available on quay.io. +The image is built on top of the [official CentOS Stream base containers](quay.io/centos/centos). + +To pull the Python 3.12 minimal container image to build on, run + +``` +podman pull quay.io/sclorg/python-312-minimal-c9s +``` + +Description +----------- + +The full container image is a universal base image to build your containerized applications on top of. However, its universal nature +means that the resulting containers it produces consume a lot of disk space. This is caused mainly by the fact that the image contains +npm, compilers, header files and some other packages one might need to install and deploy their applications. + +Because size does matter for us and our customers, we have prepared this minimal container image with very limited subset +of installed packages. There are no compilers, no header files, no npm etc and the yum package manager is replaced with a minimalistic +reimplementation called microdnf, making the resulting container images much smaller. This creates some limitations +but we provide ways to workaround them. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Limitations +----------- + +1. There is only a very limited subset of packages installed. They are choosen carefully to satisfy most of the Python apps but your app might have some special needs. +1. There is no npm and nodejs. +1. There are no compilers and header files. Installation from Python wheels should still work but compilation from a source code is not supported out of the box. + +In the next chapter, we provide three possible workarounds for the mentioned limitations of the minimal container image. + +Possible solutions for the limitations +-------------------------------------- + +### Use the full container image + +It's easy at that. If you don't want to write your own Dockerfile and disk space is not a problem, use +the full universal container image and you should be fine. + +### Build your own container image on top of the minimal container image + +Let's say that your application depends on uwsgi. uwsgi cannot be installed from Python wheel and has to be +compiled from source which requires some additional packages to be installed - namely gcc for the compilation +itself and python3.12-devel containing Python header files. + +To solve that problem, you can use all the pieces provided by the minimal container image and just add one more +step to install the missing dependencies: + +``` +FROM python-312-minimal + +# 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 + +# Install packages necessary for compiling uwsgi from source +RUN microdnf install -y gcc python3.12-devel +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +If you do it this way, your problem with the missing packages is solved. But there is also one disadvantage: the resulting +runtime image contains unnecessary compiler and Python header files. How to solve this? Uninstalling them at the end +of the Dockerfile is not really a solution but we have one. Keep reading. + +### Build on full image, run on minimal image + +Did you know that you can copy files from one image to another one during a build? That's the feature we are gonna use now. +We use the full container image with all compilers and other usefull packages installed to build our app and its dependencies +and we then move the result including the whole virtual environemnt to the minimal container image. + +This app needs mod_wsgi and to install (compile it from source) it, we'll need: httpd-devel for header files, gcc and redhat-rpm-config +as a compiler and configuratuion and finally python3.12-devel containing Python header files. There is no need to install those packages +manually because the full container image already contains them. However, the application needs httpd as a runtime dependency +so we need to install it to the minimal container image as well. + +``` +# Part 1 - build + +FROM python-312 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +# Part 2 - deploy + +FROM python-312-minimal + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd package - runtime dependency of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +This way, the resulting container image does contain only necessary dependencies and it's much lighter. diff --git a/3.12-minimal/root/opt/app-root/etc/generate_container_user b/3.12-minimal/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.12-minimal/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.12-minimal/root/opt/wheels b/3.12-minimal/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.12-minimal/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.12-minimal/root/usr/bin/cgroup-limits b/3.12-minimal/root/usr/bin/cgroup-limits new file mode 100755 index 00000000..6fa19153 --- /dev/null +++ b/3.12-minimal/root/usr/bin/cgroup-limits @@ -0,0 +1,192 @@ +#!/usr/libexec/platform-python + +""" +Script for parsing cgroup information + +This script will read some limits from the cgroup system and parse +them, printing out "VARIABLE=VALUE" on each line for every limit that is +successfully read. Output of this script can be directly fed into +bash's export command. Recommended usage from a bash script: + + set -o errexit + export_vars=$(cgroup-limits) ; export $export_vars + +Variables currently supported: + MAX_MEMORY_LIMIT_IN_BYTES + Maximum possible limit MEMORY_LIMIT_IN_BYTES can have. This is + currently constant value of 9223372036854775807. + MEMORY_LIMIT_IN_BYTES + Maximum amount of user memory in bytes. If this value is set + to the same value as MAX_MEMORY_LIMIT_IN_BYTES, it means that + there is no limit set. The value is taken from + /sys/fs/cgroup/memory/memory.limit_in_bytes for cgroups v1 + and from /sys/fs/cgroup/memory.max for cgroups v2 + NUMBER_OF_CORES + Number of CPU cores that can be used. If both the cpu and cpuset + controllers specify a limit, the controller with the lowest CPU + limit takes precedence. For the cpu controller, the value is + calculated from /sys/fs/cgroup/cpu/cpu.cfs_{quota,period}_us for + cgroups v1 and from /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2. + For the cpuset controller, the value is taken from + /sys/fs/cgroup/cpuset/cpuset.cpus for cgroups v1 and from + /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2 + NO_MEMORY_LIMIT + Set to "true" if MEMORY_LIMIT_IN_BYTES is so high that the caller + can act as if no memory limit was set. Undefined otherwise. + +Note about non-root containers: + + Per podman-run(1) man page, on some systems, changing the resource limits + may not be allowed for non-root users. For more details, see + https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-resource-limits-fails-with-a-permissions-error. + +How to test this script: + + Run a container as root and see whether the output of available memory + and CPUs match what is either available on the host or specified via + cgroups limits by the container runtime (podman). + + For example: + + # This should return NO_MEMORY_LIMIT=true, NUMBER_OF_CORES= + sudo podman run -ti --rm quay.io/fedora/s2i-core /usr/bin/cgroup-limits + + # This should return MEMORY_LIMIT_IN_BYTES=2147483648, NUMBER_OF_CORES=3 + # 3 CPUs despite --cpus 4 was given is correct, because the cpuset 2-4 means + # running on 3 concrete processors only, which is lower limit than --cpus + # and thus is preferred + sudo podman run -ti --rm --cpus 4 --cpuset-cpus=2-4 --memory 2G quay.io/fedora/s2i-core /usr/bin/cgroup-limits +""" + +from __future__ import division +from __future__ import print_function +import sys +import subprocess + + +def _read_file(path): + try: + with open(path, 'r') as f: + return f.read().strip() + except IOError: + return None + + +def get_memory_limit(): + """ + Read memory limit, in bytes. + """ + + limit = _read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') + # If first file does not exist, try cgroups v2 file + limit = limit or _read_file('/sys/fs/cgroup/memory.max') + if limit is None or not limit.isdigit(): + if limit == 'max': + return 9223372036854775807 + print("Warning: Can't detect memory limit from cgroups", + file=sys.stderr) + return None + return int(limit) + + +def get_number_of_cores(): + """ + Read number of CPU cores. + """ + + limits = [l for l in [_read_cpu_quota(), _read_cpuset_size(), _read_nproc()] if l] + if not limits: + return None + return min(limits) + + +def _read_cpu_quota(): + + quota, period = None, None + + quota = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") + if quota: + # cgroups v1 + quota = quota.strip() + if quota == "-1": + return None + + period = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us") + if period: + period = period.strip() + + else: + # cgroups v2 + line = _read_file("/sys/fs/cgroup/cpu.max") + if line: + fields = line.split() + + if len(fields) >= 2: + quota = fields[0] + if quota == "max": + return None + period = fields[1] + + + if quota and quota.isdigit() and period and period.isdigit(): + return int(quota)//int(period) + + print("Warning: Can't detect cpu quota from cgroups", + file=sys.stderr) + return None + + +def _read_cpuset_size(): + + core_count = 0 + + line = _read_file('/sys/fs/cgroup/cpuset/cpuset.cpus') + # If first file does not exist, try cgroups v2 file + line = line or _read_file('/sys/fs/cgroup/cpuset.cpus.effective') + if line is None: + # None of the files above exists when running podman as non-root, + # so in that case, this warning is printed every-time + print("Warning: Can't detect cpuset size from cgroups, will use nproc", + file=sys.stderr) + return None + + for group in line.split(','): + core_ids = list(map(int, group.split('-'))) + if len(core_ids) == 2: + core_count += core_ids[1] - core_ids[0] + 1 + else: + core_count += 1 + + return core_count + + +def _read_nproc(): + """ + Returns number of cores without looking at cgroup limits. + This might work as the last resort when running a container as non-root + where cgroups v2 resource limits cannot be set without a specific + configuration (per podman-run(1) man page). + """ + try: + stdout, stderr = subprocess.Popen('nproc', stdout=subprocess.PIPE).communicate() + return int(stdout) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return None + + +if __name__ == "__main__": + env_vars = { + "MAX_MEMORY_LIMIT_IN_BYTES": 9223372036854775807, + "MEMORY_LIMIT_IN_BYTES": get_memory_limit(), + "NUMBER_OF_CORES": get_number_of_cores() + } + + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + if env_vars.get("MEMORY_LIMIT_IN_BYTES", 0) >= 92233720368547: + env_vars["NO_MEMORY_LIMIT"] = "true" + + for key, value in env_vars.items(): + print("{0}={1}".format(key, value)) diff --git a/3.12-minimal/root/usr/bin/fix-permissions b/3.12-minimal/root/usr/bin/fix-permissions new file mode 100755 index 00000000..ddd33ace --- /dev/null +++ b/3.12-minimal/root/usr/bin/fix-permissions @@ -0,0 +1,27 @@ +#!/bin/sh + +# Allow this script to fail without failing a build +set +e + +SYMLINK_OPT=${2:--L} + +# Fix permissions on the given directory or file to allow group read/write of +# regular files and execute of directories. + +[ $(id -u) -ne 0 ] && CHECK_OWNER=" -uid $(id -u)" + +# If argument does not exist, script will still exit with 0, +# but at least we'll see something went wrong in the log +if ! [ -e "$1" ] ; then + echo "ERROR: File or directory $1 does not exist." >&2 + # We still want to end successfully + exit 0 +fi + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -gid 0 -exec chgrp 0 {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -perm -g+rw -exec chmod g+rw {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -perm /u+x -a \! -perm /g+x -exec chmod g+x {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -type d \! -perm /g+x -exec chmod g+x {} + + +# Always end successfully +exit 0 diff --git a/3.12-minimal/root/usr/bin/rpm-file-permissions b/3.12-minimal/root/usr/bin/rpm-file-permissions new file mode 100755 index 00000000..8be1fb0d --- /dev/null +++ b/3.12-minimal/root/usr/bin/rpm-file-permissions @@ -0,0 +1,21 @@ +#!/bin/sh + +CHECK_DIRS="/ /opt /etc /usr /usr/bin /usr/lib /usr/lib64 /usr/share /usr/libexec" + +rpm_format="[%{FILESTATES:fstate} %7{FILEMODES:octal} %{FILENAMES:shescape}\n]" + +rpm -q --qf "$rpm_format" filesystem | while read line +do + eval "set -- $line" + + case $1 in + normal) ;; + *) continue ;; + esac + + case " $CHECK_DIRS " in + *" $3 "*) + chmod "${2: -4}" "$3" + ;; + esac +done diff --git a/3.12-minimal/s2i/bin/assemble b/3.12-minimal/s2i/bin/assemble new file mode 100755 index 00000000..34a68dbb --- /dev/null +++ b/3.12-minimal/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.12 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.12-minimal/s2i/bin/init-wrapper b/3.12-minimal/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.12-minimal/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/3.12-minimal/s2i/bin/run b/3.12-minimal/s2i/bin/run new file mode 100755 index 00000000..bc0b3a15 --- /dev/null +++ b/3.12-minimal/s2i/bin/run @@ -0,0 +1,157 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + # Using python and just python here because the script has + # platform-python in its shebang which we don't have in the minimal image + limit_vars=$(python /usr/bin/cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.12-minimal/s2i/bin/usage b/3.12-minimal/s2i/bin/usage new file mode 100755 index 00000000..5261cd04 --- /dev/null +++ b/3.12-minimal/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.12-minimal/test/__init__.py b/3.12-minimal/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.12-minimal/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.12-minimal/test/app-home-test-app b/3.12-minimal/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.12-minimal/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.12-minimal/test/app-module-test-app b/3.12-minimal/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.12-minimal/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.12-minimal/test/check_imagestreams.py b/3.12-minimal/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.12-minimal/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.12-minimal/test/conftest.py b/3.12-minimal/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.12-minimal/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.12-minimal/test/django-different-port-test-app b/3.12-minimal/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.12-minimal/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.12-minimal/test/django-test-app b/3.12-minimal/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.12-minimal/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.12-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl b/3.12-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl new file mode 120000 index 00000000..f5c5b907 --- /dev/null +++ b/3.12-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl \ No newline at end of file diff --git a/3.12-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl b/3.12-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl new file mode 100644 index 00000000..53a88357 --- /dev/null +++ b/3.12-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl @@ -0,0 +1,20 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +ENV UPGRADE_PIP_TO_LATEST=1 + +# 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 +# Install packages necessary for compiling uwsgi from source +# pkgconfig(python-3.12) is provided by both python3-devel in c9s +# and python3.12-devel in UBI8. +RUN microdnf install -y gcc "pkgconfig(python-3.12)" which +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/3.12-minimal/test/gunicorn-config-different-port-test-app b/3.12-minimal/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.12-minimal/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.12-minimal/test/gunicorn-different-port-test-app b/3.12-minimal/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.12-minimal/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.12-minimal/test/gunicorn-python-configfile-different-port-test-app b/3.12-minimal/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.12-minimal/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.12-minimal/test/imagestreams b/3.12-minimal/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.12-minimal/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.12-minimal/test/locale-test-app b/3.12-minimal/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.12-minimal/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.12-minimal/test/micropipenv-requirements-test-app b/3.12-minimal/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.12-minimal/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/2.7/test/setup-test-app/.gitignore b/3.12-minimal/test/micropipenv-test-app/.gitignore similarity index 100% rename from 2.7/test/setup-test-app/.gitignore rename to 3.12-minimal/test/micropipenv-test-app/.gitignore diff --git a/3.12-minimal/test/micropipenv-test-app/.s2i/environment b/3.12-minimal/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.12-minimal/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/3.12-minimal/test/micropipenv-test-app/Pipfile b/3.12-minimal/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..148759cb --- /dev/null +++ b/3.12-minimal/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.12" diff --git a/3.12-minimal/test/micropipenv-test-app/Pipfile.lock b/3.12-minimal/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..1d3f18e5 --- /dev/null +++ b/3.12-minimal/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "08628d8adcf5d297b100b9dbb14807af951f225a705b902e39726cbbbaf91616" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + } + } +} diff --git a/2.7/test/setup-test-app/setup.py b/3.12-minimal/test/micropipenv-test-app/setup.py similarity index 100% rename from 2.7/test/setup-test-app/setup.py rename to 3.12-minimal/test/micropipenv-test-app/setup.py diff --git a/3.12-minimal/test/micropipenv-test-app/testapp.py b/3.12-minimal/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.12-minimal/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.12-minimal/test/numpy-test-app b/3.12-minimal/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.12-minimal/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.12-minimal/test/pipenv-and-micropipenv-should-fail-test-app b/3.12-minimal/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.12-minimal/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.3/test/setup-test-app/.gitignore b/3.12-minimal/test/pipenv-test-app/.gitignore similarity index 100% rename from 3.3/test/setup-test-app/.gitignore rename to 3.12-minimal/test/pipenv-test-app/.gitignore diff --git a/3.12-minimal/test/pipenv-test-app/.s2i/environment b/3.12-minimal/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.12-minimal/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.12-minimal/test/pipenv-test-app/Pipfile b/3.12-minimal/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..148759cb --- /dev/null +++ b/3.12-minimal/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.12" diff --git a/3.12-minimal/test/pipenv-test-app/Pipfile.lock b/3.12-minimal/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..1d3f18e5 --- /dev/null +++ b/3.12-minimal/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "08628d8adcf5d297b100b9dbb14807af951f225a705b902e39726cbbbaf91616" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + } + } +} diff --git a/3.3/test/setup-test-app/setup.py b/3.12-minimal/test/pipenv-test-app/setup.py similarity index 100% rename from 3.3/test/setup-test-app/setup.py rename to 3.12-minimal/test/pipenv-test-app/setup.py diff --git a/3.12-minimal/test/pipenv-test-app/testapp.py b/3.12-minimal/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.12-minimal/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.12-minimal/test/poetry-src-layout-test-app b/3.12-minimal/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.12-minimal/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.12-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.12-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.12-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.12-minimal/test/pyuwsgi-pipenv-test-app/app.sh b/3.12-minimal/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.12-minimal/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.12-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.12-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.12-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.12-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.12-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.12-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.12-minimal/test/run b/3.12-minimal/test/run new file mode 100755 index 00000000..8de24fe3 --- /dev/null +++ b/3.12-minimal/test/run @@ -0,0 +1,308 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${MINIMAL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile_minimal +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.12-minimal/test/run-openshift-pytest b/3.12-minimal/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.12-minimal/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.12-minimal/test/run-pytest b/3.12-minimal/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.12-minimal/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.12-minimal/test/setup-cfg-test-app b/3.12-minimal/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.12-minimal/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.12-minimal/test/setup-requirements-test-app b/3.12-minimal/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.12-minimal/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.12-minimal/test/setup-test-app b/3.12-minimal/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.12-minimal/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.12-minimal/test/standalone-custom-pypi-index-test-app b/3.12-minimal/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.12-minimal/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.12-minimal/test/standalone-test-app b/3.12-minimal/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.12-minimal/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.12-minimal/test/test-lib.sh b/3.12-minimal/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.12-minimal/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.12-minimal/test/test_container_application.py b/3.12-minimal/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.12-minimal/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.12-minimal/test/test_container_basics.py b/3.12-minimal/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.12-minimal/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_deploy_templates.py b/3.12-minimal/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.12-minimal/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_helm_python_django_app.py b/3.12-minimal/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.12-minimal/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_helm_python_django_psql_persistent.py b/3.12-minimal/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.12-minimal/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_helm_python_imagestreams.py b/3.12-minimal/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.12-minimal/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_imagestreams_quickstart.py b/3.12-minimal/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.12-minimal/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_python_ex_standalone.py b/3.12-minimal/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.12-minimal/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.12-minimal/test/test_ocp_python_ex_template.py b/3.12-minimal/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.12-minimal/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.12-minimal/test/uwsgi-test-app b/3.12-minimal/test/uwsgi-test-app new file mode 120000 index 00000000..c3ccf241 --- /dev/null +++ b/3.12-minimal/test/uwsgi-test-app @@ -0,0 +1 @@ +../../examples/uwsgi-test-app \ No newline at end of file diff --git a/3.12/Dockerfile.c10s b/3.12/Dockerfile.c10s new file mode 100644 index 00000000..0ff5e778 --- /dev/null +++ b/3.12/Dockerfile.c10s @@ -0,0 +1,88 @@ +# This image provides a Python 3.12 environment you can use to run your Python +# applications. +FROM quay.io/sclorg/s2i-base-c10s:c10s + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="$NAME" \ + name="sclorg/python-312-c10s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12/test/setup-test-app/ $FGC/$NAME python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapper-libs \ + httpd httpd-devel mod_ssl mod_auth_gssapi mod_ldap \ + mod_session gcc-gfortran libffi-devel libtool-ltdl krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12/root/ / + +# Python 3.7+ 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.12/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.12 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12/Dockerfile.c9s b/3.12/Dockerfile.c9s new file mode 100644 index 00000000..22461d24 --- /dev/null +++ b/3.12/Dockerfile.c9s @@ -0,0 +1,89 @@ +# This image provides a Python 3.12 environment you can use to run your Python +# applications. +FROM quay.io/sclorg/s2i-base-c9s:c9s + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="$NAME" \ + name="sclorg/python-312-c9s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12/test/setup-test-app/ $FGC/$NAME python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.12 python3.12-devel python3.12-pip nss_wrapper-libs httpd \ + httpd-devel mod_ssl mod_auth_gssapi mod_ldap mod_session \ + atlas-devel gcc-gfortran libffi-devel libtool-ltdl enchant \ + krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12/root/ / + +# Python 3.7+ 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.12/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.12 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12/Dockerfile.rhel8 b/3.12/Dockerfile.rhel8 new file mode 100644 index 00000000..46cb2abf --- /dev/null +++ b/3.12/Dockerfile.rhel8 @@ -0,0 +1,96 @@ +# This image provides a Python 3.12 environment you can use to run your Python +# applications. +FROM ubi8/s2i-base:1 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi8-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="ubi8/python-312" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12/test/setup-test-app/ ubi8/python-312 python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi8-python-312" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.12 python3.12-devel python3.12-setuptools python3.12-pip nss_wrapper-libs \ + httpd httpd-devel mod_ssl mod_auth_gssapi mod_ldap \ + mod_session atlas-devel gcc-gfortran libffi-devel libtool-ltdl \ + enchant krb5-devel" && \ + yum -y module enable httpd:2.4 && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + # Remove redhat-logos-httpd (httpd dependency) to keep image size smaller. + rpm -e --nodeps redhat-logos-httpd && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12/root/ / + +# Python 3.7+ 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.12/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.12 -m venv ${APP_ROOT} && \ + # Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ + # We have to upgrade pip to a newer verison because \ + # 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 && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12/Dockerfile.rhel9 b/3.12/Dockerfile.rhel9 new file mode 100644 index 00000000..27b6f4de --- /dev/null +++ b/3.12/Dockerfile.rhel9 @@ -0,0 +1,95 @@ +# This image provides a Python 3.12 environment you can use to run your Python +# applications. +FROM ubi9/s2i-base:1 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.12 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-312 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.12" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python312,python-312,rh-python312" \ + com.redhat.component="python-312-container" \ + name="ubi9/python-312" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.12/test/setup-test-app/ ubi9/python-312 python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-312" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.12 python3.12-devel python3.12-pip nss_wrapper-libs httpd \ + httpd-devel mod_ssl mod_auth_gssapi mod_ldap mod_session \ + atlas-devel gcc-gfortran libffi-devel libtool-ltdl enchant \ + krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + # Remove redhat-logos-httpd (httpd dependency) to keep image size smaller. + rpm -e --nodeps redhat-logos-httpd && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.12/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.12/root/ / + +# Python 3.7+ 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.12/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.12 -m venv ${APP_ROOT} && \ + # Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ + # We have to upgrade pip to a newer verison because \ + # 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 && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.12/README.md b/3.12/README.md new file mode 100644 index 00000000..fb526ac0 --- /dev/null +++ b/3.12/README.md @@ -0,0 +1,388 @@ +Python 3.12 container image +========================= + +This container image includes Python 3.12 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.12 applications. +Users can choose between RHEL, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +Description +----------- + +Python 3.12 available as container is a base platform for +building and running various Python 3.12 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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:3.12-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:3.12-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull registry.access.redhat.com/ubi9/python-312 +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM registry.access.redhat.com/ubi9/python-312 + +# 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 +``` + +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: + +``` +FROM registry.access.redhat.com/ubi9/python-312 + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.12/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/3.12/root/opt/app-root/etc/generate_container_user b/3.12/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.12/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.12/root/opt/wheels b/3.12/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.12/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.12/s2i/bin/assemble b/3.12/s2i/bin/assemble new file mode 100755 index 00000000..34a68dbb --- /dev/null +++ b/3.12/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.12 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.12/s2i/bin/init-wrapper b/3.12/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.12/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/3.12/s2i/bin/run b/3.12/s2i/bin/run new file mode 100755 index 00000000..7003f038 --- /dev/null +++ b/3.12/s2i/bin/run @@ -0,0 +1,155 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.12/s2i/bin/usage b/3.12/s2i/bin/usage new file mode 100755 index 00000000..ef36d6fc --- /dev/null +++ b/3.12/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.12/test/__init__.py b/3.12/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.12/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.12/test/app-home-test-app b/3.12/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.12/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.12/test/app-module-test-app b/3.12/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.12/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.12/test/check_imagestreams.py b/3.12/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.12/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.12/test/conftest.py b/3.12/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.12/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.12/test/django-different-port-test-app b/3.12/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.12/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.12/test/django-test-app b/3.12/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.12/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.12/test/from-dockerfile/Dockerfile.tpl b/3.12/test/from-dockerfile/Dockerfile.tpl new file mode 120000 index 00000000..ba88ccc4 --- /dev/null +++ b/3.12/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/Dockerfile.tpl \ No newline at end of file diff --git a/3.12/test/from-dockerfile/Dockerfile_no_s2i.tpl b/3.12/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/3.12/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/3.12/test/gunicorn-config-different-port-test-app b/3.12/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.12/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.12/test/gunicorn-different-port-test-app b/3.12/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.12/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.12/test/gunicorn-python-configfile-different-port-test-app b/3.12/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.12/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.12/test/imagestreams b/3.12/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.12/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.12/test/locale-test-app b/3.12/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.12/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.12/test/micropipenv-requirements-test-app b/3.12/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.12/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.4/test/setup-test-app/.gitignore b/3.12/test/micropipenv-test-app/.gitignore similarity index 100% rename from 3.4/test/setup-test-app/.gitignore rename to 3.12/test/micropipenv-test-app/.gitignore diff --git a/3.12/test/micropipenv-test-app/.s2i/environment b/3.12/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.12/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/3.12/test/micropipenv-test-app/Pipfile b/3.12/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..148759cb --- /dev/null +++ b/3.12/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.12" diff --git a/3.12/test/micropipenv-test-app/Pipfile.lock b/3.12/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..1d3f18e5 --- /dev/null +++ b/3.12/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "08628d8adcf5d297b100b9dbb14807af951f225a705b902e39726cbbbaf91616" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + } + } +} diff --git a/3.4/test/setup-test-app/setup.py b/3.12/test/micropipenv-test-app/setup.py similarity index 100% rename from 3.4/test/setup-test-app/setup.py rename to 3.12/test/micropipenv-test-app/setup.py diff --git a/3.12/test/micropipenv-test-app/testapp.py b/3.12/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.12/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.12/test/mod-wsgi-test-app b/3.12/test/mod-wsgi-test-app new file mode 120000 index 00000000..9d8c3338 --- /dev/null +++ b/3.12/test/mod-wsgi-test-app @@ -0,0 +1 @@ +../../examples/mod-wsgi-test-app \ No newline at end of file diff --git a/3.12/test/npm-virtualenv-uwsgi-test-app b/3.12/test/npm-virtualenv-uwsgi-test-app new file mode 120000 index 00000000..b34ae21e --- /dev/null +++ b/3.12/test/npm-virtualenv-uwsgi-test-app @@ -0,0 +1 @@ +../../examples/npm-virtualenv-uwsgi-test-app \ No newline at end of file diff --git a/3.12/test/numpy-test-app b/3.12/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.12/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.12/test/pin-pipenv-version-test-app/.s2i/environment b/3.12/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.12/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.12/test/pin-pipenv-version-test-app/app.sh b/3.12/test/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/3.12/test/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.12/test/pin-pipenv-version-test-app/requirements.txt b/3.12/test/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/3.12/test/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/3.12/test/pin-pipenv-version-test-app/wsgi.py b/3.12/test/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.12/test/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.12/test/pipenv-and-micropipenv-should-fail-test-app b/3.12/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.12/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.4/test/django-test-app/.gitignore b/3.12/test/pipenv-test-app/.gitignore similarity index 95% rename from 3.4/test/django-test-app/.gitignore rename to 3.12/test/pipenv-test-app/.gitignore index 651f49f7..ba746605 100644 --- a/3.4/test/django-test-app/.gitignore +++ b/3.12/test/pipenv-test-app/.gitignore @@ -1,7 +1,3 @@ -# Django -db.sqlite3 -staticfiles/ - # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/3.12/test/pipenv-test-app/.s2i/environment b/3.12/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.12/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.12/test/pipenv-test-app/Pipfile b/3.12/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..148759cb --- /dev/null +++ b/3.12/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.12" diff --git a/3.12/test/pipenv-test-app/Pipfile.lock b/3.12/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..1d3f18e5 --- /dev/null +++ b/3.12/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "08628d8adcf5d297b100b9dbb14807af951f225a705b902e39726cbbbaf91616" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + } + } +} diff --git a/3.12/test/pipenv-test-app/setup.py b/3.12/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.12/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/3.12/test/pipenv-test-app/testapp.py b/3.12/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.12/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.12/test/poetry-src-layout-test-app b/3.12/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.12/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.12/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.12/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.12/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.12/test/pyuwsgi-pipenv-test-app/app.sh b/3.12/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.12/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.12/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.12/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.12/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.12/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.12/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.12/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.12/test/run b/3.12/test/run new file mode 100755 index 00000000..134639eb --- /dev/null +++ b/3.12/test/run @@ -0,0 +1,312 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.12/test/run-openshift-pytest b/3.12/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.12/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.12/test/run-pytest b/3.12/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.12/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.12/test/setup-cfg-test-app b/3.12/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.12/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.12/test/setup-requirements-test-app b/3.12/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.12/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.12/test/setup-test-app b/3.12/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.12/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.12/test/standalone-custom-pypi-index-test-app b/3.12/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.12/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.12/test/standalone-test-app b/3.12/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.12/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.12/test/test-lib.sh b/3.12/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.12/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.12/test/test_container_application.py b/3.12/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.12/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.12/test/test_container_basics.py b/3.12/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.12/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.12/test/test_ocp_deploy_templates.py b/3.12/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.12/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.12/test/test_ocp_helm_python_django_app.py b/3.12/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.12/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.12/test/test_ocp_helm_python_django_psql_persistent.py b/3.12/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.12/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.12/test/test_ocp_helm_python_imagestreams.py b/3.12/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.12/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.12/test/test_ocp_imagestreams_quickstart.py b/3.12/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.12/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.12/test/test_ocp_python_ex_standalone.py b/3.12/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.12/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.12/test/test_ocp_python_ex_template.py b/3.12/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.12/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.13-minimal/Dockerfile.c10s b/3.13-minimal/Dockerfile.c10s new file mode 100644 index 00000000..34031864 --- /dev/null +++ b/3.13-minimal/Dockerfile.c10s @@ -0,0 +1,105 @@ +FROM quay.io/centos/centos:stream10-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.13 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi10-python-313 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el10" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.13" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python313,python-313,rh-python313" \ + com.redhat.component="python-313-container" \ + name="sclorg/python-313-minimal-c10s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.13-minimal/test/setup-test-app/ ubi10/python-313-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi10-python-313-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.13 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y install --setopt=install_weak_deps=0 epel-release && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.13-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.13-minimal/root/ / + +# Python 3.7+ 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.13-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.13 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.13-minimal/Dockerfile.fedora b/3.13-minimal/Dockerfile.fedora new file mode 100644 index 00000000..16f4e382 --- /dev/null +++ b/3.13-minimal/Dockerfile.fedora @@ -0,0 +1,104 @@ +FROM quay.io/fedora/fedora-minimal:42 + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.13 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi-python-313 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.13" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python313,python-313,rh-python313" \ + com.redhat.component="python-313-container" \ + name="fedora/python-313-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.13-minimal/test/setup-test-app/ ubi/python-313-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi-python-313-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.13 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.13-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.13-minimal/root/ / + +# Python 3.7+ 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.13-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.13 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.13-minimal/README.md b/3.13-minimal/README.md new file mode 100644 index 00000000..d9c56abb --- /dev/null +++ b/3.13-minimal/README.md @@ -0,0 +1,125 @@ +Python 3.13 container image - minimal version +============================================ + +This container image is a special version of the [full Python 3.13 container image](https://github.com/sclorg/s2i-python-container/tree/master/3.13) +provided as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.13 applications. + +Because the minimal and full images work similarly, we document here only the differences and limitations +of the minimal container image. For the documentation of common features see the [full container image docs](https://github.com/sclorg/s2i-python-container/tree/master/3.13). + +The Python 3.13 minimal container image is currently considered a tech-preview and only available on quay.io. +The image is built on top of the [official CentOS Stream base containers](quay.io/centos/centos). + +To pull the Python 3.13 minimal container image to build on, run + +``` +podman pull quay.io/fedora/python-313-minimal +``` + +Description +----------- + +The full container image is a universal base image to build your containerized applications on top of. However, its universal nature +means that the resulting containers it produces consume a lot of disk space. This is caused mainly by the fact that the image contains +npm, compilers, header files and some other packages one might need to install and deploy their applications. + +Because size does matter for us and our customers, we have prepared this minimal container image with very limited subset +of installed packages. There are no compilers, no header files, no npm etc and the yum package manager is replaced with a minimalistic +reimplementation called microdnf, making the resulting container images much smaller. This creates some limitations +but we provide ways to workaround them. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Limitations +----------- + +1. There is only a very limited subset of packages installed. They are choosen carefully to satisfy most of the Python apps but your app might have some special needs. +1. There is no npm and nodejs. +1. There are no compilers and header files. Installation from Python wheels should still work but compilation from a source code is not supported out of the box. + +In the next chapter, we provide three possible workarounds for the mentioned limitations of the minimal container image. + +Possible solutions for the limitations +-------------------------------------- + +### Use the full container image + +It's easy at that. If you don't want to write your own Dockerfile and disk space is not a problem, use +the full universal container image and you should be fine. + +### Build your own container image on top of the minimal container image + +Let's say that your application depends on uwsgi. uwsgi cannot be installed from Python wheel and has to be +compiled from source which requires some additional packages to be installed - namely gcc for the compilation +itself and python3.13-devel containing Python header files. + +To solve that problem, you can use all the pieces provided by the minimal container image and just add one more +step to install the missing dependencies: + +``` +FROM python-313-minimal + +# 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 + +# Install packages necessary for compiling uwsgi from source +RUN microdnf install -y gcc python3.13-devel +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +If you do it this way, your problem with the missing packages is solved. But there is also one disadvantage: the resulting +runtime image contains unnecessary compiler and Python header files. How to solve this? Uninstalling them at the end +of the Dockerfile is not really a solution but we have one. Keep reading. + +### Build on full image, run on minimal image + +Did you know that you can copy files from one image to another one during a build? That's the feature we are gonna use now. +We use the full container image with all compilers and other usefull packages installed to build our app and its dependencies +and we then move the result including the whole virtual environemnt to the minimal container image. + +This app needs mod_wsgi and to install (compile it from source) it, we'll need: httpd-devel for header files, gcc and redhat-rpm-config +as a compiler and configuratuion and finally python3.13-devel containing Python header files. There is no need to install those packages +manually because the full container image already contains them. However, the application needs httpd as a runtime dependency +so we need to install it to the minimal container image as well. + +``` +# Part 1 - build + +FROM python-313 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +# Part 2 - deploy + +FROM python-313-minimal + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd package - runtime dependency of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +This way, the resulting container image does contain only necessary dependencies and it's much lighter. diff --git a/3.13-minimal/root/opt/app-root/etc/generate_container_user b/3.13-minimal/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.13-minimal/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.13-minimal/root/opt/wheels b/3.13-minimal/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.13-minimal/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.13-minimal/root/usr/bin/cgroup-limits b/3.13-minimal/root/usr/bin/cgroup-limits new file mode 100755 index 00000000..6fa19153 --- /dev/null +++ b/3.13-minimal/root/usr/bin/cgroup-limits @@ -0,0 +1,192 @@ +#!/usr/libexec/platform-python + +""" +Script for parsing cgroup information + +This script will read some limits from the cgroup system and parse +them, printing out "VARIABLE=VALUE" on each line for every limit that is +successfully read. Output of this script can be directly fed into +bash's export command. Recommended usage from a bash script: + + set -o errexit + export_vars=$(cgroup-limits) ; export $export_vars + +Variables currently supported: + MAX_MEMORY_LIMIT_IN_BYTES + Maximum possible limit MEMORY_LIMIT_IN_BYTES can have. This is + currently constant value of 9223372036854775807. + MEMORY_LIMIT_IN_BYTES + Maximum amount of user memory in bytes. If this value is set + to the same value as MAX_MEMORY_LIMIT_IN_BYTES, it means that + there is no limit set. The value is taken from + /sys/fs/cgroup/memory/memory.limit_in_bytes for cgroups v1 + and from /sys/fs/cgroup/memory.max for cgroups v2 + NUMBER_OF_CORES + Number of CPU cores that can be used. If both the cpu and cpuset + controllers specify a limit, the controller with the lowest CPU + limit takes precedence. For the cpu controller, the value is + calculated from /sys/fs/cgroup/cpu/cpu.cfs_{quota,period}_us for + cgroups v1 and from /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2. + For the cpuset controller, the value is taken from + /sys/fs/cgroup/cpuset/cpuset.cpus for cgroups v1 and from + /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2 + NO_MEMORY_LIMIT + Set to "true" if MEMORY_LIMIT_IN_BYTES is so high that the caller + can act as if no memory limit was set. Undefined otherwise. + +Note about non-root containers: + + Per podman-run(1) man page, on some systems, changing the resource limits + may not be allowed for non-root users. For more details, see + https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-resource-limits-fails-with-a-permissions-error. + +How to test this script: + + Run a container as root and see whether the output of available memory + and CPUs match what is either available on the host or specified via + cgroups limits by the container runtime (podman). + + For example: + + # This should return NO_MEMORY_LIMIT=true, NUMBER_OF_CORES= + sudo podman run -ti --rm quay.io/fedora/s2i-core /usr/bin/cgroup-limits + + # This should return MEMORY_LIMIT_IN_BYTES=2147483648, NUMBER_OF_CORES=3 + # 3 CPUs despite --cpus 4 was given is correct, because the cpuset 2-4 means + # running on 3 concrete processors only, which is lower limit than --cpus + # and thus is preferred + sudo podman run -ti --rm --cpus 4 --cpuset-cpus=2-4 --memory 2G quay.io/fedora/s2i-core /usr/bin/cgroup-limits +""" + +from __future__ import division +from __future__ import print_function +import sys +import subprocess + + +def _read_file(path): + try: + with open(path, 'r') as f: + return f.read().strip() + except IOError: + return None + + +def get_memory_limit(): + """ + Read memory limit, in bytes. + """ + + limit = _read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') + # If first file does not exist, try cgroups v2 file + limit = limit or _read_file('/sys/fs/cgroup/memory.max') + if limit is None or not limit.isdigit(): + if limit == 'max': + return 9223372036854775807 + print("Warning: Can't detect memory limit from cgroups", + file=sys.stderr) + return None + return int(limit) + + +def get_number_of_cores(): + """ + Read number of CPU cores. + """ + + limits = [l for l in [_read_cpu_quota(), _read_cpuset_size(), _read_nproc()] if l] + if not limits: + return None + return min(limits) + + +def _read_cpu_quota(): + + quota, period = None, None + + quota = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") + if quota: + # cgroups v1 + quota = quota.strip() + if quota == "-1": + return None + + period = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us") + if period: + period = period.strip() + + else: + # cgroups v2 + line = _read_file("/sys/fs/cgroup/cpu.max") + if line: + fields = line.split() + + if len(fields) >= 2: + quota = fields[0] + if quota == "max": + return None + period = fields[1] + + + if quota and quota.isdigit() and period and period.isdigit(): + return int(quota)//int(period) + + print("Warning: Can't detect cpu quota from cgroups", + file=sys.stderr) + return None + + +def _read_cpuset_size(): + + core_count = 0 + + line = _read_file('/sys/fs/cgroup/cpuset/cpuset.cpus') + # If first file does not exist, try cgroups v2 file + line = line or _read_file('/sys/fs/cgroup/cpuset.cpus.effective') + if line is None: + # None of the files above exists when running podman as non-root, + # so in that case, this warning is printed every-time + print("Warning: Can't detect cpuset size from cgroups, will use nproc", + file=sys.stderr) + return None + + for group in line.split(','): + core_ids = list(map(int, group.split('-'))) + if len(core_ids) == 2: + core_count += core_ids[1] - core_ids[0] + 1 + else: + core_count += 1 + + return core_count + + +def _read_nproc(): + """ + Returns number of cores without looking at cgroup limits. + This might work as the last resort when running a container as non-root + where cgroups v2 resource limits cannot be set without a specific + configuration (per podman-run(1) man page). + """ + try: + stdout, stderr = subprocess.Popen('nproc', stdout=subprocess.PIPE).communicate() + return int(stdout) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return None + + +if __name__ == "__main__": + env_vars = { + "MAX_MEMORY_LIMIT_IN_BYTES": 9223372036854775807, + "MEMORY_LIMIT_IN_BYTES": get_memory_limit(), + "NUMBER_OF_CORES": get_number_of_cores() + } + + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + if env_vars.get("MEMORY_LIMIT_IN_BYTES", 0) >= 92233720368547: + env_vars["NO_MEMORY_LIMIT"] = "true" + + for key, value in env_vars.items(): + print("{0}={1}".format(key, value)) diff --git a/3.13-minimal/root/usr/bin/fix-permissions b/3.13-minimal/root/usr/bin/fix-permissions new file mode 100755 index 00000000..ddd33ace --- /dev/null +++ b/3.13-minimal/root/usr/bin/fix-permissions @@ -0,0 +1,27 @@ +#!/bin/sh + +# Allow this script to fail without failing a build +set +e + +SYMLINK_OPT=${2:--L} + +# Fix permissions on the given directory or file to allow group read/write of +# regular files and execute of directories. + +[ $(id -u) -ne 0 ] && CHECK_OWNER=" -uid $(id -u)" + +# If argument does not exist, script will still exit with 0, +# but at least we'll see something went wrong in the log +if ! [ -e "$1" ] ; then + echo "ERROR: File or directory $1 does not exist." >&2 + # We still want to end successfully + exit 0 +fi + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -gid 0 -exec chgrp 0 {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -perm -g+rw -exec chmod g+rw {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -perm /u+x -a \! -perm /g+x -exec chmod g+x {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -type d \! -perm /g+x -exec chmod g+x {} + + +# Always end successfully +exit 0 diff --git a/3.13-minimal/root/usr/bin/rpm-file-permissions b/3.13-minimal/root/usr/bin/rpm-file-permissions new file mode 100755 index 00000000..8be1fb0d --- /dev/null +++ b/3.13-minimal/root/usr/bin/rpm-file-permissions @@ -0,0 +1,21 @@ +#!/bin/sh + +CHECK_DIRS="/ /opt /etc /usr /usr/bin /usr/lib /usr/lib64 /usr/share /usr/libexec" + +rpm_format="[%{FILESTATES:fstate} %7{FILEMODES:octal} %{FILENAMES:shescape}\n]" + +rpm -q --qf "$rpm_format" filesystem | while read line +do + eval "set -- $line" + + case $1 in + normal) ;; + *) continue ;; + esac + + case " $CHECK_DIRS " in + *" $3 "*) + chmod "${2: -4}" "$3" + ;; + esac +done diff --git a/3.13-minimal/s2i/bin/assemble b/3.13-minimal/s2i/bin/assemble new file mode 100755 index 00000000..f415fe30 --- /dev/null +++ b/3.13-minimal/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.13 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.13-minimal/s2i/bin/init-wrapper b/3.13-minimal/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/s2i/bin/run b/3.13-minimal/s2i/bin/run new file mode 100755 index 00000000..bc0b3a15 --- /dev/null +++ b/3.13-minimal/s2i/bin/run @@ -0,0 +1,157 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + # Using python and just python here because the script has + # platform-python in its shebang which we don't have in the minimal image + limit_vars=$(python /usr/bin/cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.13-minimal/s2i/bin/usage b/3.13-minimal/s2i/bin/usage new file mode 100755 index 00000000..77e0aa9f --- /dev/null +++ b/3.13-minimal/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.13-minimal/test/__init__.py b/3.13-minimal/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.13-minimal/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.13-minimal/test/app-home-test-app b/3.13-minimal/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.13-minimal/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.13-minimal/test/app-module-test-app b/3.13-minimal/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.13-minimal/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.13-minimal/test/check_imagestreams.py b/3.13-minimal/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.13-minimal/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.13-minimal/test/conftest.py b/3.13-minimal/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.13-minimal/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.13-minimal/test/django-different-port-test-app b/3.13-minimal/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.13-minimal/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.13-minimal/test/django-test-app b/3.13-minimal/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.13-minimal/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.13-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl b/3.13-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl new file mode 120000 index 00000000..f5c5b907 --- /dev/null +++ b/3.13-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl \ No newline at end of file diff --git a/3.13-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl b/3.13-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl new file mode 100644 index 00000000..599866eb --- /dev/null +++ b/3.13-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl @@ -0,0 +1,20 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +ENV UPGRADE_PIP_TO_LATEST=1 + +# 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 +# Install packages necessary for compiling uwsgi from source +# pkgconfig(python-3.13) is provided by both python3-devel in c9s +# and python3.13-devel in UBI8. +RUN microdnf install -y gcc "pkgconfig(python-3.13)" which +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/3.13-minimal/test/gunicorn-config-different-port-test-app b/3.13-minimal/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.13-minimal/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.13-minimal/test/gunicorn-different-port-test-app b/3.13-minimal/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.13-minimal/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.13-minimal/test/gunicorn-python-configfile-different-port-test-app b/3.13-minimal/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.13-minimal/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.13-minimal/test/imagestreams b/3.13-minimal/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.13-minimal/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.13-minimal/test/locale-test-app b/3.13-minimal/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.13-minimal/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.13-minimal/test/micropipenv-requirements-test-app b/3.13-minimal/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.13-minimal/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.13-minimal/test/micropipenv-test-app/.gitignore b/3.13-minimal/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/test/micropipenv-test-app/.s2i/environment b/3.13-minimal/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/test/micropipenv-test-app/Pipfile b/3.13-minimal/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..fc4d0c80 --- /dev/null +++ b/3.13-minimal/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.13" diff --git a/3.13-minimal/test/micropipenv-test-app/Pipfile.lock b/3.13-minimal/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..de1ccaf4 --- /dev/null +++ b/3.13-minimal/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "index": "pypi", + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "version": "==8.3.3" + } + } +} diff --git a/3.13-minimal/test/micropipenv-test-app/setup.py b/3.13-minimal/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/test/micropipenv-test-app/testapp.py b/3.13-minimal/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.13-minimal/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.13-minimal/test/numpy-test-app b/3.13-minimal/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.13-minimal/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.13-minimal/test/pipenv-and-micropipenv-should-fail-test-app b/3.13-minimal/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.13-minimal/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.13-minimal/test/pipenv-test-app/.gitignore b/3.13-minimal/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/test/pipenv-test-app/.s2i/environment b/3.13-minimal/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.13-minimal/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.13-minimal/test/pipenv-test-app/Pipfile b/3.13-minimal/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..fc4d0c80 --- /dev/null +++ b/3.13-minimal/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.13" diff --git a/3.13-minimal/test/pipenv-test-app/Pipfile.lock b/3.13-minimal/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..de1ccaf4 --- /dev/null +++ b/3.13-minimal/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "index": "pypi", + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "version": "==8.3.3" + } + } +} diff --git a/3.13-minimal/test/pipenv-test-app/setup.py b/3.13-minimal/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.13-minimal/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/3.13-minimal/test/pipenv-test-app/testapp.py b/3.13-minimal/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.13-minimal/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.13-minimal/test/poetry-src-layout-test-app b/3.13-minimal/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.13-minimal/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.13-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.13-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.13-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.13-minimal/test/pyuwsgi-pipenv-test-app/app.sh b/3.13-minimal/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.13-minimal/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.13-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.13-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.13-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.13-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.13-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.13-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.13-minimal/test/run b/3.13-minimal/test/run new file mode 100755 index 00000000..8de24fe3 --- /dev/null +++ b/3.13-minimal/test/run @@ -0,0 +1,308 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${MINIMAL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile_minimal +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.13-minimal/test/run-openshift-pytest b/3.13-minimal/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.13-minimal/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.13-minimal/test/run-pytest b/3.13-minimal/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.13-minimal/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.13-minimal/test/setup-cfg-test-app b/3.13-minimal/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.13-minimal/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.13-minimal/test/setup-requirements-test-app b/3.13-minimal/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.13-minimal/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.13-minimal/test/setup-test-app b/3.13-minimal/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.13-minimal/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.13-minimal/test/standalone-custom-pypi-index-test-app b/3.13-minimal/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.13-minimal/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.13-minimal/test/standalone-test-app b/3.13-minimal/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.13-minimal/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.13-minimal/test/test-lib.sh b/3.13-minimal/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.13-minimal/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.13-minimal/test/test_container_application.py b/3.13-minimal/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.13-minimal/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.13-minimal/test/test_container_basics.py b/3.13-minimal/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.13-minimal/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_deploy_templates.py b/3.13-minimal/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.13-minimal/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_helm_python_django_app.py b/3.13-minimal/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.13-minimal/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_helm_python_django_psql_persistent.py b/3.13-minimal/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.13-minimal/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_helm_python_imagestreams.py b/3.13-minimal/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.13-minimal/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_imagestreams_quickstart.py b/3.13-minimal/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.13-minimal/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_python_ex_standalone.py b/3.13-minimal/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.13-minimal/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.13-minimal/test/test_ocp_python_ex_template.py b/3.13-minimal/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.13-minimal/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.13-minimal/test/uwsgi-test-app b/3.13-minimal/test/uwsgi-test-app new file mode 120000 index 00000000..c3ccf241 --- /dev/null +++ b/3.13-minimal/test/uwsgi-test-app @@ -0,0 +1 @@ +../../examples/uwsgi-test-app \ No newline at end of file diff --git a/3.13/Dockerfile.c10s b/3.13/Dockerfile.c10s new file mode 100644 index 00000000..87196fe3 --- /dev/null +++ b/3.13/Dockerfile.c10s @@ -0,0 +1,89 @@ +# This image provides a Python 3.13 environment you can use to run your Python +# applications. +FROM quay.io/sclorg/s2i-base-c10s:c10s + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.13 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.13" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python313,python-313,rh-python313" \ + com.redhat.component="$NAME" \ + name="sclorg/python-313-c10s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.13/test/setup-test-app/ $FGC/$NAME python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.13 python3.13-devel python3.13-pip nss_wrapper-libs httpd \ + httpd-devel mod_ssl mod_auth_gssapi mod_ldap mod_session \ + gcc-gfortran libffi-devel libtool-ltdl krb5-devel" && \ + yum -y install --setopt=install_weak_deps=0 epel-release && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.13/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.13/root/ / + +# Python 3.7+ 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.13/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.13 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.13/Dockerfile.fedora b/3.13/Dockerfile.fedora new file mode 100644 index 00000000..c8b9bbdd --- /dev/null +++ b/3.13/Dockerfile.fedora @@ -0,0 +1,88 @@ +# This image provides a Python 3.13 environment you can use to run your Python +# applications. +FROM quay.io/fedora/s2i-base:42 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.13 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.13" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python313,python-313,rh-python313" \ + com.redhat.component="$NAME" \ + name="fedora/$NAME-313" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.13/test/setup-test-app/ quay.io/fedora/$NAME-313 python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapper-libs \ + httpd httpd-devel atlas-devel gcc-gfortran libffi-devel \ + libtool-ltdl enchant redhat-rpm-config krb5-devel" && \ + dnf -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + dnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.13/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.13/root/ / + +# Python 3.7+ 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.13/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.13 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.13/README.md b/3.13/README.md new file mode 100644 index 00000000..7bd4ddfd --- /dev/null +++ b/3.13/README.md @@ -0,0 +1,388 @@ +Python 3.13 container image +========================= + +This container image includes Python 3.13 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.13 applications. +Users can choose between RHEL, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +Description +----------- + +Python 3.13 available as container is a base platform for +building and running various Python 3.13 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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:3.13-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:3.13-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull quay.io/fedora/python-313 +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM quay.io/fedora/python-313 + +# 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 +``` + +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: + +``` +FROM quay.io/fedora/python-313 + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.13/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/3.13/root/opt/app-root/etc/generate_container_user b/3.13/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.13/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.13/root/opt/wheels b/3.13/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.13/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.13/s2i/bin/assemble b/3.13/s2i/bin/assemble new file mode 100755 index 00000000..f415fe30 --- /dev/null +++ b/3.13/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.13 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.13/s2i/bin/init-wrapper b/3.13/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.13/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/3.13/s2i/bin/run b/3.13/s2i/bin/run new file mode 100755 index 00000000..7003f038 --- /dev/null +++ b/3.13/s2i/bin/run @@ -0,0 +1,155 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.13/s2i/bin/usage b/3.13/s2i/bin/usage new file mode 100755 index 00000000..7ee134dd --- /dev/null +++ b/3.13/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.13/test/__init__.py b/3.13/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.13/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.13/test/app-home-test-app b/3.13/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.13/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.13/test/app-module-test-app b/3.13/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.13/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.13/test/check_imagestreams.py b/3.13/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.13/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.13/test/conftest.py b/3.13/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.13/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.13/test/django-different-port-test-app b/3.13/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.13/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.13/test/django-test-app b/3.13/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.13/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.13/test/from-dockerfile/Dockerfile.tpl b/3.13/test/from-dockerfile/Dockerfile.tpl new file mode 120000 index 00000000..ba88ccc4 --- /dev/null +++ b/3.13/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/Dockerfile.tpl \ No newline at end of file diff --git a/3.13/test/from-dockerfile/Dockerfile_no_s2i.tpl b/3.13/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/3.13/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/3.13/test/gunicorn-config-different-port-test-app b/3.13/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.13/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.13/test/gunicorn-different-port-test-app b/3.13/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.13/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.13/test/gunicorn-python-configfile-different-port-test-app b/3.13/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.13/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.13/test/imagestreams b/3.13/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.13/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.13/test/locale-test-app b/3.13/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.13/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.13/test/micropipenv-requirements-test-app b/3.13/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.13/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.13/test/micropipenv-test-app/.gitignore b/3.13/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.13/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/3.13/test/micropipenv-test-app/.s2i/environment b/3.13/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.13/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/3.13/test/micropipenv-test-app/Pipfile b/3.13/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..fc4d0c80 --- /dev/null +++ b/3.13/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.13" diff --git a/3.13/test/micropipenv-test-app/Pipfile.lock b/3.13/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..de1ccaf4 --- /dev/null +++ b/3.13/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "index": "pypi", + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "version": "==8.3.3" + } + } +} diff --git a/3.13/test/micropipenv-test-app/setup.py b/3.13/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.13/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/3.13/test/micropipenv-test-app/testapp.py b/3.13/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.13/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.13/test/mod-wsgi-test-app b/3.13/test/mod-wsgi-test-app new file mode 120000 index 00000000..9d8c3338 --- /dev/null +++ b/3.13/test/mod-wsgi-test-app @@ -0,0 +1 @@ +../../examples/mod-wsgi-test-app \ No newline at end of file diff --git a/3.13/test/npm-virtualenv-uwsgi-test-app b/3.13/test/npm-virtualenv-uwsgi-test-app new file mode 120000 index 00000000..b34ae21e --- /dev/null +++ b/3.13/test/npm-virtualenv-uwsgi-test-app @@ -0,0 +1 @@ +../../examples/npm-virtualenv-uwsgi-test-app \ No newline at end of file diff --git a/3.13/test/numpy-test-app b/3.13/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.13/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.13/test/pin-pipenv-version-test-app/.s2i/environment b/3.13/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.13/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.13/test/pin-pipenv-version-test-app/app.sh b/3.13/test/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/3.13/test/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.13/test/pin-pipenv-version-test-app/requirements.txt b/3.13/test/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/3.13/test/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/3.13/test/pin-pipenv-version-test-app/wsgi.py b/3.13/test/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.13/test/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.13/test/pipenv-and-micropipenv-should-fail-test-app b/3.13/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.13/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.13/test/pipenv-test-app/.gitignore b/3.13/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.13/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/3.13/test/pipenv-test-app/.s2i/environment b/3.13/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.13/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.13/test/pipenv-test-app/Pipfile b/3.13/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..fc4d0c80 --- /dev/null +++ b/3.13/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.13" diff --git a/3.13/test/pipenv-test-app/Pipfile.lock b/3.13/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..de1ccaf4 --- /dev/null +++ b/3.13/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "index": "pypi", + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "version": "==8.3.3" + } + } +} diff --git a/3.13/test/pipenv-test-app/setup.py b/3.13/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.13/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/3.13/test/pipenv-test-app/testapp.py b/3.13/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.13/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.13/test/poetry-src-layout-test-app b/3.13/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.13/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.13/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.13/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..160e51c4 --- /dev/null +++ b/3.13/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2023.11.14 diff --git a/3.13/test/pyuwsgi-pipenv-test-app/app.sh b/3.13/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.13/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.13/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.13/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.13/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.13/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.13/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.13/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.13/test/run b/3.13/test/run new file mode 100755 index 00000000..134639eb --- /dev/null +++ b/3.13/test/run @@ -0,0 +1,312 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.13/test/run-openshift-pytest b/3.13/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.13/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.13/test/run-pytest b/3.13/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.13/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.13/test/setup-cfg-test-app b/3.13/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.13/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.13/test/setup-requirements-test-app b/3.13/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.13/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.13/test/setup-test-app b/3.13/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.13/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.13/test/standalone-custom-pypi-index-test-app b/3.13/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.13/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.13/test/standalone-test-app b/3.13/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.13/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.13/test/test-lib.sh b/3.13/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.13/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.13/test/test_container_application.py b/3.13/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.13/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.13/test/test_container_basics.py b/3.13/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.13/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.13/test/test_ocp_deploy_templates.py b/3.13/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.13/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.13/test/test_ocp_helm_python_django_app.py b/3.13/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.13/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.13/test/test_ocp_helm_python_django_psql_persistent.py b/3.13/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.13/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.13/test/test_ocp_helm_python_imagestreams.py b/3.13/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.13/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.13/test/test_ocp_imagestreams_quickstart.py b/3.13/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.13/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.13/test/test_ocp_python_ex_standalone.py b/3.13/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.13/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.13/test/test_ocp_python_ex_template.py b/3.13/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.13/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.14-minimal/Dockerfile.c10s b/3.14-minimal/Dockerfile.c10s new file mode 100644 index 00000000..5c1cf880 --- /dev/null +++ b/3.14-minimal/Dockerfile.c10s @@ -0,0 +1,104 @@ +FROM quay.io/centos/centos:stream10-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi10-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el10" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="sclorg/python-314-minimal-c10s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14-minimal/test/setup-test-app/ ubi10/python-314-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi10-python-314-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.14 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14-minimal/root/ / + +# Python 3.7+ 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.14-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.14 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14-minimal/Dockerfile.c9s b/3.14-minimal/Dockerfile.c9s new file mode 100644 index 00000000..9c9883b8 --- /dev/null +++ b/3.14-minimal/Dockerfile.c9s @@ -0,0 +1,104 @@ +FROM quay.io/centos/centos:stream9-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el9" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="sclorg/python-314-minimal-c9s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14-minimal/test/setup-test-app/ ubi9/python-314-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-314-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.14 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14-minimal/root/ / + +# Python 3.7+ 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.14-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.14 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14-minimal/Dockerfile.fedora b/3.14-minimal/Dockerfile.fedora new file mode 100644 index 00000000..ca0fde42 --- /dev/null +++ b/3.14-minimal/Dockerfile.fedora @@ -0,0 +1,104 @@ +FROM quay.io/fedora/fedora-minimal:43 + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="fedora/python-314-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14-minimal/test/setup-test-app/ ubi/python-314-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi-python-314-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.14 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14-minimal/root/ / + +# Python 3.7+ 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.14-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.14 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14-minimal/Dockerfile.rhel10 b/3.14-minimal/Dockerfile.rhel10 new file mode 100644 index 00000000..cad80210 --- /dev/null +++ b/3.14-minimal/Dockerfile.rhel10 @@ -0,0 +1,104 @@ +FROM ubi10-minimal:latest + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi10-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el10" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="ubi10/python-314-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14-minimal/test/setup-test-app/ ubi10/python-314-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi10-python-314-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.14 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14-minimal/root/ / + +# Python 3.7+ 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.14-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.14 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14-minimal/Dockerfile.rhel9 b/3.14-minimal/Dockerfile.rhel9 new file mode 100644 index 00000000..0eea5238 --- /dev/null +++ b/3.14-minimal/Dockerfile.rhel9 @@ -0,0 +1,104 @@ +FROM ubi9/ubi-minimal:latest + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el9" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="ubi9/python-314-minimal" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14-minimal/test/setup-test-app/ ubi9/python-314-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-314-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python3.14 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14-minimal/root/ / + +# Python 3.7+ 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.14-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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.14 -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14-minimal/README.md b/3.14-minimal/README.md new file mode 100644 index 00000000..8a24e8f4 --- /dev/null +++ b/3.14-minimal/README.md @@ -0,0 +1,125 @@ +Python 3.14 container image - minimal version +============================================ + +This container image is a special version of the [full Python 3.14 container image](https://github.com/sclorg/s2i-python-container/tree/master/3.14) +provided as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.14 applications. + +Because the minimal and full images work similarly, we document here only the differences and limitations +of the minimal container image. For the documentation of common features see the [full container image docs](https://github.com/sclorg/s2i-python-container/tree/master/3.14). + +The Python 3.14 minimal container image is currently considered a tech-preview and only available on quay.io. +The image is built on top of the [official CentOS Stream base containers](quay.io/centos/centos). + +To pull the Python 3.14 minimal container image to build on, run + +``` +podman pull quay.io/fedora/python-314-minimal +``` + +Description +----------- + +The full container image is a universal base image to build your containerized applications on top of. However, its universal nature +means that the resulting containers it produces consume a lot of disk space. This is caused mainly by the fact that the image contains +npm, compilers, header files and some other packages one might need to install and deploy their applications. + +Because size does matter for us and our customers, we have prepared this minimal container image with very limited subset +of installed packages. There are no compilers, no header files, no npm etc and the yum package manager is replaced with a minimalistic +reimplementation called microdnf, making the resulting container images much smaller. This creates some limitations +but we provide ways to workaround them. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Limitations +----------- + +1. There is only a very limited subset of packages installed. They are choosen carefully to satisfy most of the Python apps but your app might have some special needs. +1. There is no npm and nodejs. +1. There are no compilers and header files. Installation from Python wheels should still work but compilation from a source code is not supported out of the box. + +In the next chapter, we provide three possible workarounds for the mentioned limitations of the minimal container image. + +Possible solutions for the limitations +-------------------------------------- + +### Use the full container image + +It's easy at that. If you don't want to write your own Dockerfile and disk space is not a problem, use +the full universal container image and you should be fine. + +### Build your own container image on top of the minimal container image + +Let's say that your application depends on uwsgi. uwsgi cannot be installed from Python wheel and has to be +compiled from source which requires some additional packages to be installed - namely gcc for the compilation +itself and python3.14-devel containing Python header files. + +To solve that problem, you can use all the pieces provided by the minimal container image and just add one more +step to install the missing dependencies: + +``` +FROM python-314-minimal + +# 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 + +# Install packages necessary for compiling uwsgi from source +RUN microdnf install -y gcc python3.14-devel +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +If you do it this way, your problem with the missing packages is solved. But there is also one disadvantage: the resulting +runtime image contains unnecessary compiler and Python header files. How to solve this? Uninstalling them at the end +of the Dockerfile is not really a solution but we have one. Keep reading. + +### Build on full image, run on minimal image + +Did you know that you can copy files from one image to another one during a build? That's the feature we are gonna use now. +We use the full container image with all compilers and other usefull packages installed to build our app and its dependencies +and we then move the result including the whole virtual environemnt to the minimal container image. + +This app needs mod_wsgi and to install (compile it from source) it, we'll need: httpd-devel for header files, gcc and redhat-rpm-config +as a compiler and configuratuion and finally python3.14-devel containing Python header files. There is no need to install those packages +manually because the full container image already contains them. However, the application needs httpd as a runtime dependency +so we need to install it to the minimal container image as well. + +``` +# Part 1 - build + +FROM python-314 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +# Part 2 - deploy + +FROM python-314-minimal + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd package - runtime dependency of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +This way, the resulting container image does contain only necessary dependencies and it's much lighter. diff --git a/3.14-minimal/root/opt/app-root/etc/generate_container_user b/3.14-minimal/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.14-minimal/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.14-minimal/root/opt/wheels b/3.14-minimal/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.14-minimal/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.14-minimal/root/usr/bin/cgroup-limits b/3.14-minimal/root/usr/bin/cgroup-limits new file mode 100755 index 00000000..6fa19153 --- /dev/null +++ b/3.14-minimal/root/usr/bin/cgroup-limits @@ -0,0 +1,192 @@ +#!/usr/libexec/platform-python + +""" +Script for parsing cgroup information + +This script will read some limits from the cgroup system and parse +them, printing out "VARIABLE=VALUE" on each line for every limit that is +successfully read. Output of this script can be directly fed into +bash's export command. Recommended usage from a bash script: + + set -o errexit + export_vars=$(cgroup-limits) ; export $export_vars + +Variables currently supported: + MAX_MEMORY_LIMIT_IN_BYTES + Maximum possible limit MEMORY_LIMIT_IN_BYTES can have. This is + currently constant value of 9223372036854775807. + MEMORY_LIMIT_IN_BYTES + Maximum amount of user memory in bytes. If this value is set + to the same value as MAX_MEMORY_LIMIT_IN_BYTES, it means that + there is no limit set. The value is taken from + /sys/fs/cgroup/memory/memory.limit_in_bytes for cgroups v1 + and from /sys/fs/cgroup/memory.max for cgroups v2 + NUMBER_OF_CORES + Number of CPU cores that can be used. If both the cpu and cpuset + controllers specify a limit, the controller with the lowest CPU + limit takes precedence. For the cpu controller, the value is + calculated from /sys/fs/cgroup/cpu/cpu.cfs_{quota,period}_us for + cgroups v1 and from /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2. + For the cpuset controller, the value is taken from + /sys/fs/cgroup/cpuset/cpuset.cpus for cgroups v1 and from + /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2 + NO_MEMORY_LIMIT + Set to "true" if MEMORY_LIMIT_IN_BYTES is so high that the caller + can act as if no memory limit was set. Undefined otherwise. + +Note about non-root containers: + + Per podman-run(1) man page, on some systems, changing the resource limits + may not be allowed for non-root users. For more details, see + https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-resource-limits-fails-with-a-permissions-error. + +How to test this script: + + Run a container as root and see whether the output of available memory + and CPUs match what is either available on the host or specified via + cgroups limits by the container runtime (podman). + + For example: + + # This should return NO_MEMORY_LIMIT=true, NUMBER_OF_CORES= + sudo podman run -ti --rm quay.io/fedora/s2i-core /usr/bin/cgroup-limits + + # This should return MEMORY_LIMIT_IN_BYTES=2147483648, NUMBER_OF_CORES=3 + # 3 CPUs despite --cpus 4 was given is correct, because the cpuset 2-4 means + # running on 3 concrete processors only, which is lower limit than --cpus + # and thus is preferred + sudo podman run -ti --rm --cpus 4 --cpuset-cpus=2-4 --memory 2G quay.io/fedora/s2i-core /usr/bin/cgroup-limits +""" + +from __future__ import division +from __future__ import print_function +import sys +import subprocess + + +def _read_file(path): + try: + with open(path, 'r') as f: + return f.read().strip() + except IOError: + return None + + +def get_memory_limit(): + """ + Read memory limit, in bytes. + """ + + limit = _read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') + # If first file does not exist, try cgroups v2 file + limit = limit or _read_file('/sys/fs/cgroup/memory.max') + if limit is None or not limit.isdigit(): + if limit == 'max': + return 9223372036854775807 + print("Warning: Can't detect memory limit from cgroups", + file=sys.stderr) + return None + return int(limit) + + +def get_number_of_cores(): + """ + Read number of CPU cores. + """ + + limits = [l for l in [_read_cpu_quota(), _read_cpuset_size(), _read_nproc()] if l] + if not limits: + return None + return min(limits) + + +def _read_cpu_quota(): + + quota, period = None, None + + quota = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") + if quota: + # cgroups v1 + quota = quota.strip() + if quota == "-1": + return None + + period = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us") + if period: + period = period.strip() + + else: + # cgroups v2 + line = _read_file("/sys/fs/cgroup/cpu.max") + if line: + fields = line.split() + + if len(fields) >= 2: + quota = fields[0] + if quota == "max": + return None + period = fields[1] + + + if quota and quota.isdigit() and period and period.isdigit(): + return int(quota)//int(period) + + print("Warning: Can't detect cpu quota from cgroups", + file=sys.stderr) + return None + + +def _read_cpuset_size(): + + core_count = 0 + + line = _read_file('/sys/fs/cgroup/cpuset/cpuset.cpus') + # If first file does not exist, try cgroups v2 file + line = line or _read_file('/sys/fs/cgroup/cpuset.cpus.effective') + if line is None: + # None of the files above exists when running podman as non-root, + # so in that case, this warning is printed every-time + print("Warning: Can't detect cpuset size from cgroups, will use nproc", + file=sys.stderr) + return None + + for group in line.split(','): + core_ids = list(map(int, group.split('-'))) + if len(core_ids) == 2: + core_count += core_ids[1] - core_ids[0] + 1 + else: + core_count += 1 + + return core_count + + +def _read_nproc(): + """ + Returns number of cores without looking at cgroup limits. + This might work as the last resort when running a container as non-root + where cgroups v2 resource limits cannot be set without a specific + configuration (per podman-run(1) man page). + """ + try: + stdout, stderr = subprocess.Popen('nproc', stdout=subprocess.PIPE).communicate() + return int(stdout) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return None + + +if __name__ == "__main__": + env_vars = { + "MAX_MEMORY_LIMIT_IN_BYTES": 9223372036854775807, + "MEMORY_LIMIT_IN_BYTES": get_memory_limit(), + "NUMBER_OF_CORES": get_number_of_cores() + } + + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + if env_vars.get("MEMORY_LIMIT_IN_BYTES", 0) >= 92233720368547: + env_vars["NO_MEMORY_LIMIT"] = "true" + + for key, value in env_vars.items(): + print("{0}={1}".format(key, value)) diff --git a/3.14-minimal/root/usr/bin/fix-permissions b/3.14-minimal/root/usr/bin/fix-permissions new file mode 100755 index 00000000..ddd33ace --- /dev/null +++ b/3.14-minimal/root/usr/bin/fix-permissions @@ -0,0 +1,27 @@ +#!/bin/sh + +# Allow this script to fail without failing a build +set +e + +SYMLINK_OPT=${2:--L} + +# Fix permissions on the given directory or file to allow group read/write of +# regular files and execute of directories. + +[ $(id -u) -ne 0 ] && CHECK_OWNER=" -uid $(id -u)" + +# If argument does not exist, script will still exit with 0, +# but at least we'll see something went wrong in the log +if ! [ -e "$1" ] ; then + echo "ERROR: File or directory $1 does not exist." >&2 + # We still want to end successfully + exit 0 +fi + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -gid 0 -exec chgrp 0 {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -perm -g+rw -exec chmod g+rw {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -perm /u+x -a \! -perm /g+x -exec chmod g+x {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -type d \! -perm /g+x -exec chmod g+x {} + + +# Always end successfully +exit 0 diff --git a/3.14-minimal/root/usr/bin/rpm-file-permissions b/3.14-minimal/root/usr/bin/rpm-file-permissions new file mode 100755 index 00000000..8be1fb0d --- /dev/null +++ b/3.14-minimal/root/usr/bin/rpm-file-permissions @@ -0,0 +1,21 @@ +#!/bin/sh + +CHECK_DIRS="/ /opt /etc /usr /usr/bin /usr/lib /usr/lib64 /usr/share /usr/libexec" + +rpm_format="[%{FILESTATES:fstate} %7{FILEMODES:octal} %{FILENAMES:shescape}\n]" + +rpm -q --qf "$rpm_format" filesystem | while read line +do + eval "set -- $line" + + case $1 in + normal) ;; + *) continue ;; + esac + + case " $CHECK_DIRS " in + *" $3 "*) + chmod "${2: -4}" "$3" + ;; + esac +done diff --git a/3.14-minimal/s2i/bin/assemble b/3.14-minimal/s2i/bin/assemble new file mode 100755 index 00000000..b5cfeb6f --- /dev/null +++ b/3.14-minimal/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.14 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.14-minimal/s2i/bin/init-wrapper b/3.14-minimal/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/s2i/bin/run b/3.14-minimal/s2i/bin/run new file mode 100755 index 00000000..bc0b3a15 --- /dev/null +++ b/3.14-minimal/s2i/bin/run @@ -0,0 +1,157 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + # Using python and just python here because the script has + # platform-python in its shebang which we don't have in the minimal image + limit_vars=$(python /usr/bin/cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.14-minimal/s2i/bin/usage b/3.14-minimal/s2i/bin/usage new file mode 100755 index 00000000..8b449de5 --- /dev/null +++ b/3.14-minimal/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.14-minimal/test/__init__.py b/3.14-minimal/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.14-minimal/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.14-minimal/test/app-home-test-app b/3.14-minimal/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.14-minimal/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.14-minimal/test/app-module-test-app b/3.14-minimal/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.14-minimal/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.14-minimal/test/check_imagestreams.py b/3.14-minimal/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.14-minimal/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.14-minimal/test/conftest.py b/3.14-minimal/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.14-minimal/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.14-minimal/test/django-different-port-test-app b/3.14-minimal/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.14-minimal/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.14-minimal/test/django-test-app b/3.14-minimal/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.14-minimal/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.14-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl b/3.14-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl new file mode 120000 index 00000000..f5c5b907 --- /dev/null +++ b/3.14-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl \ No newline at end of file diff --git a/3.14-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl b/3.14-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl new file mode 100644 index 00000000..874927da --- /dev/null +++ b/3.14-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl @@ -0,0 +1,20 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +ENV UPGRADE_PIP_TO_LATEST=1 + +# 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 +# Install packages necessary for compiling uwsgi from source +# pkgconfig(python-3.14) is provided by both python3-devel in c9s +# and python3.14-devel in UBI8. +RUN microdnf install -y gcc "pkgconfig(python-3.14)" which +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/3.14-minimal/test/gunicorn-config-different-port-test-app b/3.14-minimal/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.14-minimal/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.14-minimal/test/gunicorn-different-port-test-app b/3.14-minimal/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.14-minimal/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.14-minimal/test/gunicorn-python-configfile-different-port-test-app b/3.14-minimal/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.14-minimal/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.14-minimal/test/imagestreams b/3.14-minimal/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.14-minimal/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.14-minimal/test/locale-test-app b/3.14-minimal/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.14-minimal/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.14-minimal/test/micropipenv-requirements-test-app b/3.14-minimal/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.14-minimal/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.14-minimal/test/micropipenv-test-app/.gitignore b/3.14-minimal/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/test/micropipenv-test-app/.s2i/environment b/3.14-minimal/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/test/micropipenv-test-app/Pipfile b/3.14-minimal/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..45aee705 --- /dev/null +++ b/3.14-minimal/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.14" diff --git a/3.14-minimal/test/micropipenv-test-app/Pipfile.lock b/3.14-minimal/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/3.14-minimal/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/3.14-minimal/test/micropipenv-test-app/setup.py b/3.14-minimal/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/test/micropipenv-test-app/testapp.py b/3.14-minimal/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.14-minimal/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.14-minimal/test/numpy-test-app b/3.14-minimal/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.14-minimal/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.14-minimal/test/pipenv-and-micropipenv-should-fail-test-app b/3.14-minimal/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.14-minimal/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.14-minimal/test/pipenv-test-app/.gitignore b/3.14-minimal/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/test/pipenv-test-app/.s2i/environment b/3.14-minimal/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.14-minimal/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.14-minimal/test/pipenv-test-app/Pipfile b/3.14-minimal/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..45aee705 --- /dev/null +++ b/3.14-minimal/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.14" diff --git a/3.14-minimal/test/pipenv-test-app/Pipfile.lock b/3.14-minimal/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/3.14-minimal/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/3.14-minimal/test/pipenv-test-app/setup.py b/3.14-minimal/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.14-minimal/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/3.14-minimal/test/pipenv-test-app/testapp.py b/3.14-minimal/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.14-minimal/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.14-minimal/test/poetry-src-layout-test-app b/3.14-minimal/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.14-minimal/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.14-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.14-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..530e67f1 --- /dev/null +++ b/3.14-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2026.0.3 diff --git a/3.14-minimal/test/pyuwsgi-pipenv-test-app/app.sh b/3.14-minimal/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.14-minimal/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.14-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.14-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.14-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.14-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.14-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.14-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.14-minimal/test/run b/3.14-minimal/test/run new file mode 100755 index 00000000..8de24fe3 --- /dev/null +++ b/3.14-minimal/test/run @@ -0,0 +1,308 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${MINIMAL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile_minimal +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.14-minimal/test/run-openshift-pytest b/3.14-minimal/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.14-minimal/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.14-minimal/test/run-pytest b/3.14-minimal/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.14-minimal/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.14-minimal/test/setup-cfg-test-app b/3.14-minimal/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.14-minimal/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.14-minimal/test/setup-requirements-test-app b/3.14-minimal/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.14-minimal/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.14-minimal/test/setup-test-app b/3.14-minimal/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.14-minimal/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.14-minimal/test/standalone-custom-pypi-index-test-app b/3.14-minimal/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.14-minimal/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.14-minimal/test/standalone-test-app b/3.14-minimal/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.14-minimal/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.14-minimal/test/test-lib.sh b/3.14-minimal/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.14-minimal/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.14-minimal/test/test_container_application.py b/3.14-minimal/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.14-minimal/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.14-minimal/test/test_container_basics.py b/3.14-minimal/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.14-minimal/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_deploy_templates.py b/3.14-minimal/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.14-minimal/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_helm_python_django_app.py b/3.14-minimal/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.14-minimal/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_helm_python_django_psql_persistent.py b/3.14-minimal/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.14-minimal/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_helm_python_imagestreams.py b/3.14-minimal/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.14-minimal/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_imagestreams_quickstart.py b/3.14-minimal/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.14-minimal/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_python_ex_standalone.py b/3.14-minimal/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.14-minimal/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.14-minimal/test/test_ocp_python_ex_template.py b/3.14-minimal/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.14-minimal/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.14-minimal/test/uwsgi-test-app b/3.14-minimal/test/uwsgi-test-app new file mode 120000 index 00000000..c3ccf241 --- /dev/null +++ b/3.14-minimal/test/uwsgi-test-app @@ -0,0 +1 @@ +../../examples/uwsgi-test-app \ No newline at end of file diff --git a/3.14/Dockerfile.c9s b/3.14/Dockerfile.c9s new file mode 100644 index 00000000..329aeaa7 --- /dev/null +++ b/3.14/Dockerfile.c9s @@ -0,0 +1,89 @@ +# This image provides a Python 3.14 environment you can use to run your Python +# applications. +FROM quay.io/sclorg/s2i-base-c9s:c9s + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="$NAME" \ + name="sclorg/python-314-c9s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14/test/setup-test-app/ $FGC/$NAME python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.14 python3.14-devel python3.14-pip nss_wrapper-libs httpd \ + httpd-devel mod_ssl mod_auth_gssapi mod_ldap mod_session \ + atlas-devel gcc-gfortran libffi-devel libtool-ltdl enchant \ + krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14/root/ / + +# Python 3.7+ 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.14/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.14 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14/Dockerfile.fedora b/3.14/Dockerfile.fedora new file mode 100644 index 00000000..b46f3b3f --- /dev/null +++ b/3.14/Dockerfile.fedora @@ -0,0 +1,88 @@ +# This image provides a Python 3.14 environment you can use to run your Python +# applications. +FROM quay.io/fedora/s2i-base:43 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="$NAME" \ + name="fedora/$NAME-314" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14/test/setup-test-app/ quay.io/fedora/$NAME-314 python-sample-app" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapper-libs \ + httpd httpd-devel atlas-devel gcc-gfortran libffi-devel \ + libtool-ltdl enchant redhat-rpm-config krb5-devel" && \ + dnf -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + dnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14/root/ / + +# Python 3.7+ 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.14/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.14 -m venv ${APP_ROOT} && \ +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14/Dockerfile.rhel9 b/3.14/Dockerfile.rhel9 new file mode 100644 index 00000000..44854fd9 --- /dev/null +++ b/3.14/Dockerfile.rhel9 @@ -0,0 +1,95 @@ +# This image provides a Python 3.14 environment you can use to run your Python +# applications. +FROM ubi9/s2i-base:1 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.14 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-314 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.14" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python314,python-314,rh-python314" \ + com.redhat.component="python-314-container" \ + name="ubi9/python-314" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.14/test/setup-test-app/ ubi9/python-314 python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-314" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3.14 python3.14-devel python3.14-pip nss_wrapper-libs httpd \ + httpd-devel mod_ssl mod_auth_gssapi mod_ldap mod_session \ + atlas-devel gcc-gfortran libffi-devel libtool-ltdl enchant \ + krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + # Remove redhat-logos-httpd (httpd dependency) to keep image size smaller. + rpm -e --nodeps redhat-logos-httpd && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.14/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.14/root/ / + +# Python 3.7+ 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.14/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.14 -m venv ${APP_ROOT} && \ + # Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ + # We have to upgrade pip to a newer verison because \ + # 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 && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.14/README.md b/3.14/README.md new file mode 100644 index 00000000..5193a5bc --- /dev/null +++ b/3.14/README.md @@ -0,0 +1,388 @@ +Python 3.14 container image +========================= + +This container image includes Python 3.14 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.14 applications. +Users can choose between RHEL, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +Description +----------- + +Python 3.14 available as container is a base platform for +building and running various Python 3.14 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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:3.14-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:3.14-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull quay.io/fedora/python-314 +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM quay.io/fedora/python-314 + +# 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 +``` + +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: + +``` +FROM quay.io/fedora/python-314 + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.14/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/3.14/root/opt/app-root/etc/generate_container_user b/3.14/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.14/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.14/root/opt/wheels b/3.14/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.14/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.14/s2i/bin/assemble b/3.14/s2i/bin/assemble new file mode 100755 index 00000000..b5cfeb6f --- /dev/null +++ b/3.14/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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.14 -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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.14/s2i/bin/init-wrapper b/3.14/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.14/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/3.14/s2i/bin/run b/3.14/s2i/bin/run new file mode 100755 index 00000000..7003f038 --- /dev/null +++ b/3.14/s2i/bin/run @@ -0,0 +1,155 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.14/s2i/bin/usage b/3.14/s2i/bin/usage new file mode 100755 index 00000000..9c6009c8 --- /dev/null +++ b/3.14/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.14/test/__init__.py b/3.14/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.14/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.14/test/app-home-test-app b/3.14/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.14/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.14/test/app-module-test-app b/3.14/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.14/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.14/test/check_imagestreams.py b/3.14/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.14/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.14/test/conftest.py b/3.14/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.14/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.14/test/django-different-port-test-app b/3.14/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.14/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.14/test/django-test-app b/3.14/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.14/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.14/test/from-dockerfile/Dockerfile.tpl b/3.14/test/from-dockerfile/Dockerfile.tpl new file mode 120000 index 00000000..ba88ccc4 --- /dev/null +++ b/3.14/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/Dockerfile.tpl \ No newline at end of file diff --git a/3.14/test/from-dockerfile/Dockerfile_no_s2i.tpl b/3.14/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/3.14/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/3.14/test/gunicorn-config-different-port-test-app b/3.14/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.14/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.14/test/gunicorn-different-port-test-app b/3.14/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.14/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.14/test/gunicorn-python-configfile-different-port-test-app b/3.14/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.14/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.14/test/imagestreams b/3.14/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.14/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.14/test/locale-test-app b/3.14/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.14/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.14/test/micropipenv-requirements-test-app b/3.14/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.14/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.14/test/micropipenv-test-app/.gitignore b/3.14/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.14/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/3.14/test/micropipenv-test-app/.s2i/environment b/3.14/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.14/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/3.14/test/micropipenv-test-app/Pipfile b/3.14/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..45aee705 --- /dev/null +++ b/3.14/test/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.14" diff --git a/3.14/test/micropipenv-test-app/Pipfile.lock b/3.14/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/3.14/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/3.14/test/micropipenv-test-app/setup.py b/3.14/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.14/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/3.14/test/micropipenv-test-app/testapp.py b/3.14/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.14/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.14/test/mod-wsgi-test-app b/3.14/test/mod-wsgi-test-app new file mode 120000 index 00000000..9d8c3338 --- /dev/null +++ b/3.14/test/mod-wsgi-test-app @@ -0,0 +1 @@ +../../examples/mod-wsgi-test-app \ No newline at end of file diff --git a/3.14/test/npm-virtualenv-uwsgi-test-app b/3.14/test/npm-virtualenv-uwsgi-test-app new file mode 120000 index 00000000..b34ae21e --- /dev/null +++ b/3.14/test/npm-virtualenv-uwsgi-test-app @@ -0,0 +1 @@ +../../examples/npm-virtualenv-uwsgi-test-app \ No newline at end of file diff --git a/3.14/test/numpy-test-app b/3.14/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.14/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.14/test/pin-pipenv-version-test-app/.s2i/environment b/3.14/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..530e67f1 --- /dev/null +++ b/3.14/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2026.0.3 diff --git a/3.14/test/pin-pipenv-version-test-app/app.sh b/3.14/test/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/3.14/test/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.14/test/pin-pipenv-version-test-app/requirements.txt b/3.14/test/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/3.14/test/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/3.14/test/pin-pipenv-version-test-app/wsgi.py b/3.14/test/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.14/test/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.14/test/pipenv-and-micropipenv-should-fail-test-app b/3.14/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.14/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.14/test/pipenv-test-app/.gitignore b/3.14/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.14/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/3.14/test/pipenv-test-app/.s2i/environment b/3.14/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.14/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.14/test/pipenv-test-app/Pipfile b/3.14/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..45aee705 --- /dev/null +++ b/3.14/test/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.14" diff --git a/3.14/test/pipenv-test-app/Pipfile.lock b/3.14/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/3.14/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/3.14/test/pipenv-test-app/setup.py b/3.14/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.14/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/3.14/test/pipenv-test-app/testapp.py b/3.14/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.14/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.14/test/poetry-src-layout-test-app b/3.14/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.14/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.14/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.14/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..530e67f1 --- /dev/null +++ b/3.14/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2026.0.3 diff --git a/3.14/test/pyuwsgi-pipenv-test-app/app.sh b/3.14/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.14/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.14/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.14/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.14/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.14/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.14/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.14/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.14/test/run b/3.14/test/run new file mode 100755 index 00000000..134639eb --- /dev/null +++ b/3.14/test/run @@ -0,0 +1,312 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.14/test/run-openshift-pytest b/3.14/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.14/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.14/test/run-pytest b/3.14/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.14/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.14/test/setup-cfg-test-app b/3.14/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.14/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.14/test/setup-requirements-test-app b/3.14/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.14/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.14/test/setup-test-app b/3.14/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.14/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.14/test/standalone-custom-pypi-index-test-app b/3.14/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.14/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.14/test/standalone-test-app b/3.14/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.14/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.14/test/test-lib.sh b/3.14/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.14/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.14/test/test_container_application.py b/3.14/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.14/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.14/test/test_container_basics.py b/3.14/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.14/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.14/test/test_ocp_deploy_templates.py b/3.14/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.14/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.14/test/test_ocp_helm_python_django_app.py b/3.14/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.14/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.14/test/test_ocp_helm_python_django_psql_persistent.py b/3.14/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.14/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.14/test/test_ocp_helm_python_imagestreams.py b/3.14/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.14/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.14/test/test_ocp_imagestreams_quickstart.py b/3.14/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.14/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.14/test/test_ocp_python_ex_standalone.py b/3.14/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.14/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.14/test/test_ocp_python_ex_template.py b/3.14/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.14/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.3/Dockerfile b/3.3/Dockerfile deleted file mode 100644 index a144fb4c..00000000 --- a/3.3/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -# This image provides a Python 3.3 environment you can use to run your Python -# applications. -FROM openshift/base-centos7 - -MAINTAINER SoftwareCollections.org - -EXPOSE 8080 - -ENV PYTHON_VERSION=3.3 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 3.3 applications" \ - io.k8s.display-name="Python 3.3" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python33" - -RUN yum install -y centos-release-scl && \ - INSTALL_PKGS="python33 python33-python-devel python33-python-setuptools epel-release nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs --enablerepo=centosplus $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove centos-logos (httpd dependency, ~20MB of graphics) to keep image - # size smaller. - rpm -e --nodeps centos-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# 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 scl enable python33 "easy_install pip==1.5.6" && \ - chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/3.3/Dockerfile.rhel7 b/3.3/Dockerfile.rhel7 deleted file mode 100644 index a1dbcaaa..00000000 --- a/3.3/Dockerfile.rhel7 +++ /dev/null @@ -1,47 +0,0 @@ -# This image provides a Python 3.3 environment you can use to run your Python -# applications. -FROM openshift/base-rhel7 - -EXPOSE 8080 - -ENV PYTHON_VERSION=3.3 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 3.3 applications" \ - io.k8s.display-name="Python 3.3" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python33" \ - # Labels consumed by Red Hat build service. - BZComponent="openshift-sti-python-docker" \ - Name="openshift3/python-33-rhel7" \ - Version="3.3" \ - Release="1" \ - Architecture="x86_64" - -RUN yum-config-manager --enable rhel-server-rhscl-7-rpms && \ - yum-config-manager --enable rhel-7-server-optional-rpms && \ - yum-config-manager --enable rhel-7-server-ose-3.0-rpms && \ - yum-config-manager --disable epel >/dev/null || : && \ - INSTALL_PKGS="python33 python33-python-devel python33-python-setuptools python33-python-pip nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove redhat-logos (httpd dependency) to keep image size smaller. - rpm -e --nodeps redhat-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# In order to drop the root user, we have to make some directories world -# writable as OpenShift default security model is to run the container under -# random UID. -RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/3.3/README.md b/3.3/README.md deleted file mode 100644 index 979d4908..00000000 --- a/3.3/README.md +++ /dev/null @@ -1,205 +0,0 @@ -Python Docker image -=================== - -This repository contains the source for building various versions of -the Python application as a reproducible Docker image using -[source-to-image](https://github.com/openshift/source-to-image). -Users can choose between RHEL and CentOS based builder images. -The resulting image can be run using [Docker](http://docker.io). - - -Usage ---------------------- -To build a simple [python-sample-app](https://github.com/openshift/sti-python/tree/master/3.3/test/setup-test-app) application -using standalone [S2I](https://github.com/openshift/source-to-image) and then run the -resulting image with [Docker](http://docker.io) execute: - -* **For RHEL based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=3.3/test/setup-test-app/ openshift/python-33-rhel7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -* **For CentOS based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=3.3/test/setup-test-app/ openshift/python-33-centos7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -**Accessing the application:** -``` -$ curl 127.0.0.1:8080 -``` - - -Repository organization ------------------------- -* **``** - - * **Dockerfile** - - CentOS based Dockerfile. - - * **Dockerfile.rhel7** - - RHEL based Dockerfile. In order to perform build or test actions on this - Dockerfile you need to run the action on a properly subscribed RHEL machine. - - * **`s2i/bin/`** - - This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): - - * **assemble** - - Used to install the sources into the location where the application - will be run and prepare the application for deployment (eg. installing - dependencies, etc.) - - * **run** - - This script is responsible for running the application by using the - application web server. - - * **usage*** - - This script prints the usage of this image. - - * **`contrib/`** - - This folder contains a file with commonly used modules. - - * **`test/`** - - This folder contains a [S2I](https://github.com/openshift/source-to-image) - test framework with a simple server. - - * **`setup-test-app/`** - - Simple Gunicorn application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **`standalone-test-app/`** - - Simple standalone application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **run** - - Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. - - -Environment variables ---------------------- - -To set these environment variables, you can place them as a key value pair into a `.sti/environment` -file inside your source code repository. - -* **APP_FILE** - - Used to run the application from a Python script. - This should be a path to a Python file (defaults to `app.py`) that will be - passed to the Python interpreter to start the application. - -* **APP_MODULE** - - Used to run the application with Gunicorn, as documented - [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). - This variable specifies a WSGI callable with the pattern - `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path - of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the - specified module. - Gunicorn will look for a WSGI callable named `application` if not specified. - - If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` - file in your project and use it if it exists. - - If using `setup.py` for installing the application, the `MODULE_NAME` part - can be read from there. For an example, see - [setup-test-app](https://github.com/openshift/sti-python/tree/master/3.3/test/setup-test-app). - -* **APP_CONFIG** - - Path to a valid Python file with a - [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. - -* **DISABLE_COLLECTSTATIC** - - Set this variable to a non-empty value to inhibit the execution of - 'manage.py collectstatic' during the build. This only affects Django projects. - -* **DISABLE_MIGRATE** - - Set this variable to a non-empty value to inhibit the execution of 'manage.py migrate' - when the produced image is run. This only affects Django projects. - -Source repository layout ------------------------- - -You do not need to change anything in your existing Python project's repository. -However, if these files exist they will affect the behavior of the build process: - -* **requirements.txt** - - List of dependencies to be installed with `pip`. The format is documented - [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). - - -* **setup.py** - - Configures various aspects of the project, including installation of - dependencies, as documented - [here](https://packaging.python.org/en/latest/distributing.html#setup-py). - For most projects, it is sufficient to simply use `requirements.txt`. - - -Run strategies --------------- - -The Docker image produced by sti-python executes your project in one of the -following ways, in precedence order: - -* **Gunicorn** - - The Gunicorn WSGI HTTP server is used to serve your application in the case that it - is installed. It can be installed by listing it either in the `requirements.txt` - file or in the `install_requires` section of the `setup.py` file. - - If a file named `wsgi.py` is present in your repository, it will be used as - the entry point to your application. This can be overridden with the - environment variable `APP_MODULE`. - This file is present in Django projects by default. - - If you have both Django and Gunicorn in your requirements, your Django project - will automatically be served using Gunicorn. - -* **Django development server** - - If you have Django in your requirements but don't have Gunicorn, then your - application will be served using Django's development web server. However, this is not - recommended for production environments. - -* **Python script** - - This is the most general way of executing your application. It will be used - in the case where you specify a path to a Python script via the `APP_FILE` environment - variable, defaulting to a file named `app.py` if it exists. The script is - passed to a regular Python interpreter to launch your application. - -Hot deploy ---------------------- - -If you are using Django, hot deploy will work out of the box. - -To enable hot deploy while using Gunicorn, make sure you have a Gunicorn -configuration file inside your repository with the -[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) -option set to `true`. Make sure to specify your config via the `APP_CONFIG` -environment variable. - -To change your source code in running container, use Docker's -[exec](https://docs.docker.com/reference/commandline/exec/) command: - -``` -docker exec -it /bin/bash -``` - -After you enter into the running container, your current directory is set -to `/opt/app-root/src`, where the source code is located. diff --git a/3.3/contrib/etc/generate_container_user b/3.3/contrib/etc/generate_container_user deleted file mode 100644 index 229b986a..00000000 --- a/3.3/contrib/etc/generate_container_user +++ /dev/null @@ -1,9 +0,0 @@ -# Set current user in nss_wrapper -PASSWD_DIR="/opt/app-root/etc" - -export USER_ID=$(id -u) -export GROUP_ID=$(id -g) -envsubst < ${PASSWD_DIR}/passwd.template > ${PASSWD_DIR}/passwd -export LD_PRELOAD=libnss_wrapper.so -export NSS_WRAPPER_PASSWD=${PASSWD_DIR}/passwd -export NSS_WRAPPER_GROUP=/etc/group diff --git a/3.3/contrib/etc/passwd.template b/3.3/contrib/etc/passwd.template deleted file mode 100644 index af0814d9..00000000 --- a/3.3/contrib/etc/passwd.template +++ /dev/null @@ -1,14 +0,0 @@ -root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -operator:x:11:0:operator:/root:/sbin/nologin -games:x:12:100:games:/usr/games:/sbin/nologin -ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin -nobody:x:99:99:Nobody:/:/sbin/nologin -default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/sbin/nologin diff --git a/3.3/contrib/etc/scl_enable b/3.3/contrib/etc/scl_enable deleted file mode 100644 index ed8423a4..00000000 --- a/3.3/contrib/etc/scl_enable +++ /dev/null @@ -1,5 +0,0 @@ -# IMPORTANT: Do not add more content to this file unless you know what you are -# 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 python33 diff --git a/3.3/s2i/bin/assemble b/3.3/s2i/bin/assemble deleted file mode 100755 index 60ae7ff1..00000000 --- a/3.3/s2i/bin/assemble +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_collectstatic() { - is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] -} - -set -e - -echo "---> Copying application source ..." -cp -Rf /tmp/src/. ./ - -if [[ -f requirements.txt ]]; then - echo "---> Installing dependencies ..." - pip install --user -r requirements.txt -fi - -if [[ -f setup.py ]]; then - echo "---> Installing application ..." - python setup.py develop --user -fi - -if should_collectstatic; then - ( - echo "---> Collecting Django static files ..." - - # Find shallowest manage.py script, either ./manage.py or /manage.py - manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - - if [[ ! -f "$manage_file" ]]; then - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "'manage.py collectstatic' ignored." - exit - fi - - if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then - echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" - echo " $ python $manage_file collectstatic --noinput" - echo "Ignore this warning if you're not serving static files with Django." - exit - fi - - python $manage_file collectstatic --noinput - ) -fi - -# Fix source directory permissions -fix-permissions ./ -# set permissions for any installed artifacts -fix-permissions /opt/app-root - -# remove pip temporary directory -rm -rf /tmp/pip_build_default diff --git a/3.3/s2i/bin/run b/3.3/s2i/bin/run deleted file mode 100755 index 1c03566c..00000000 --- a/3.3/s2i/bin/run +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -source /opt/app-root/etc/generate_container_user - -function is_gunicorn_installed() { - hash gunicorn &>/dev/null -} - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_migrate() { - is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] -} - -set -e - -APP_FILE="${APP_FILE:-app.py}" -if [[ -f "$APP_FILE" ]]; then - echo "---> Running application from Python script ($APP_FILE) ..." - exec python "$APP_FILE" -fi -echo "WARNING: file '$APP_FILE' not found." - -# Find shallowest manage.py script, either ./manage.py or /manage.py -manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - -if should_migrate; then - if [[ -f "$manage_file" ]]; then - echo "---> Migrating database ..." - python "$manage_file" migrate --noinput - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py migrate'." - fi -fi - -if is_gunicorn_installed; then - if [[ -z "$APP_MODULE" ]]; then - # Find shallowest wsgi.py file, one of ./wsgi.py, /wsgi.py or //wsgi.py, - # replace "/" with "." and remove ".py" suffix - APP_MODULE=$(find . -maxdepth 3 -type f -name 'wsgi.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1 | sed 's:/:.:;s:.py$::') - fi - - if [[ -z "$APP_MODULE" && -f setup.py ]]; then - APP_MODULE="$(python setup.py --name)" - fi - - if [[ "$APP_MODULE" ]]; then - echo "---> Serving application with gunicorn ($APP_MODULE) ..." - exec gunicorn "$APP_MODULE" --bind=0.0.0.0:8080 --access-logfile=- --config "$APP_CONFIG" - fi -fi - -if is_django_installed; then - if [[ -f "$manage_file" ]]; then - echo "---> Serving application with 'manage.py runserver' ..." - echo "WARNING: this is NOT a recommended way to run you application in production!" - echo "Consider using gunicorn or some other production web server." - exec python "$manage_file" runserver 0.0.0.0:8080 - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py runserver'." - fi -fi - ->&2 echo "ERROR: don't know how to run your application." ->&2 echo "Please set either APP_MODULE or APP_FILE environment variables, or create a file 'app.py' to launch your application." -exit 1 diff --git a/3.3/s2i/bin/usage b/3.3/s2i/bin/usage deleted file mode 100755 index 99d19ae5..00000000 --- a/3.3/s2i/bin/usage +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` - -cat </dev/null -} - -container_exists() { - image_exists $(cat $cid_file) -} - -container_ip() { - docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) -} - -run_s2i_build() { - info "Building the ${1} application image ..." - s2i build ${s2i_args} file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp -} - -prepare() { - if ! image_exists ${IMAGE_NAME}; then - echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." - exit 1 - fi - # TODO: S2I build require the application is a valid 'GIT' repository, we - # should remove this restriction in the future when a file:// is used. - info "Preparing to test ${1} ..." - pushd ${test_dir}/${1} >/dev/null - git init - git config user.email "build@localhost" && git config user.name "builder" - git add -A && git commit -m "Sample commit" - popd >/dev/null -} - -run_test_application() { - docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} -p ${test_port} ${IMAGE_NAME}-testapp -} - -cleanup_app() { - info "Cleaning up app container ..." - if [ -f $cid_file ]; then - if container_exists; then - docker stop $(cat $cid_file) - fi - fi -} - -cleanup() { - info "Cleaning up the test application image" - if image_exists ${IMAGE_NAME}-testapp; then - docker rmi -f ${IMAGE_NAME}-testapp - fi - rm -rf ${test_dir}/${1}/.git -} - -check_result() { - local result="$1" - if [[ "$result" != "0" ]]; then - info "TEST FAILED (${result})" - cleanup - exit $result - fi -} - -wait_for_cid() { - local max_attempts=10 - local sleep_time=1 - local attempt=1 - local result=1 - info "Waiting for application container to start $CONTAINER_ARGS ..." - while [ $attempt -le $max_attempts ]; do - [ -f $cid_file ] && [ -s $cid_file ] && break - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done -} - -test_s2i_usage() { - info "Testing 's2i usage' ..." - s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null -} - -test_docker_run_usage() { - info "Testing 'docker run' usage ..." - docker run ${IMAGE_NAME} &>/dev/null -} - -test_scl_usage() { - local run_cmd="$1" - local expected="$2" - local cid_file="$3" - - info "Testing the image SCL enable" - out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi -} - -test_connection() { - info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." - local max_attempts=30 - local sleep_time=1 - local attempt=1 - local result=1 - while [ $attempt -le $max_attempts ]; do - response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) - status=$? - if [ $status -eq 0 ]; then - if [ $response_code -eq 200 ]; then - result=0 - fi - break - fi - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done - return $result -} - -test_application() { - local cid_file=$(mktemp -u --suffix=.cid) - # Verify that the HTTP connection can be established to test application container - run_test_application & - - # Wait for the container to write it's CID file - wait_for_cid - - test_scl_usage "python --version" "Python 3.3.2" "${cid_file}" - check_result $? - test_connection - check_result $? - cleanup_app -} - - -# Since we built the candidate image locally, we don't want S2I attempt to pull -# it from Docker hub -s2i_args="--force-pull=false" - -# Verify the 'usage' script is working properly when running the base image with 's2i usage ...' -test_s2i_usage -check_result $? - -# Verify the 'usage' script is working properly when running the base image with 'docker run ...' -test_docker_run_usage -check_result $? - -for app in ${WEB_APPS[@]}; do - prepare ${app} - run_s2i_build ${app} - check_result $? - - # test application with default user - test_application - - # test application with random user - CONTAINER_ARGS="-u 12345" test_application - - info "All tests for the ${app} finished successfully." - cleanup ${app} -done - -info "All tests finished successfully." diff --git a/3.3/test/standalone-test-app/requirements.txt b/3.3/test/standalone-test-app/requirements.txt deleted file mode 100644 index 8f22dccf..00000000 --- a/3.3/test/standalone-test-app/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gunicorn diff --git a/3.4/Dockerfile b/3.4/Dockerfile deleted file mode 100644 index 46516cac..00000000 --- a/3.4/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -# This image provides a Python 3.4 environment you can use to run your Python -# applications. -FROM openshift/base-centos7 - -MAINTAINER SoftwareCollections.org - -EXPOSE 8080 - -ENV PYTHON_VERSION=3.4 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 3.4 applications" \ - io.k8s.display-name="Python 3.4" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python34,rh-python34" - -RUN yum install -y centos-release-scl && \ - INSTALL_PKGS="rh-python34 rh-python34-python-devel rh-python34-python-setuptools rh-python34-python-pip epel-release nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs --enablerepo=centosplus $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove centos-logos (httpd dependency, ~20MB of graphics) to keep image - # size smaller. - rpm -e --nodeps centos-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# In order to drop the root user, we have to make some directories world -# writable as OpenShift default security model is to run the container under -# random UID. -RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/3.4/Dockerfile.rhel7 b/3.4/Dockerfile.rhel7 deleted file mode 100644 index e4559771..00000000 --- a/3.4/Dockerfile.rhel7 +++ /dev/null @@ -1,47 +0,0 @@ -# This image provides a Python 3.4 environment you can use to run your Python -# applications. -FROM openshift/base-rhel7 - -EXPOSE 8080 - -ENV PYTHON_VERSION=3.4 \ - PATH=$HOME/.local/bin/:$PATH - -LABEL io.k8s.description="Platform for building and running Python 3.4 applications" \ - io.k8s.display-name="Python 3.4" \ - io.openshift.expose-services="8080:http" \ - io.openshift.tags="builder,python,python34,rh-python34" \ - # Labels consumed by Red Hat build service. - BZComponent="rh-python34-docker" \ - Name="rhscl/python-34-rhel7" \ - Version="3.4" \ - Release="1" \ - Architecture="x86_64" - -RUN yum-config-manager --enable rhel-server-rhscl-7-rpms && \ - yum-config-manager --enable rhel-7-server-optional-rpms && \ - yum-config-manager --enable rhel-7-server-ose-3.0-rpms && \ - yum-config-manager --disable epel >/dev/null || : && \ - INSTALL_PKGS="rh-python34 rh-python34-python-devel rh-python34-python-setuptools rh-python34-python-pip nss_wrapper httpd httpd-devel" && \ - yum install -y --setopt=tsflags=nodocs $INSTALL_PKGS && \ - rpm -V $INSTALL_PKGS && \ - # Remove redhat-logos (httpd dependency) to keep image size smaller. - rpm -e --nodeps redhat-logos && \ - yum clean all -y - -# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. -COPY ./s2i/bin/ $STI_SCRIPTS_PATH - -# Each language image can have 'contrib' a directory with extra files needed to -# run and build the applications. -COPY ./contrib/ /opt/app-root - -# In order to drop the root user, we have to make some directories world -# writable as OpenShift default security model is to run the container under -# random UID. -RUN chown -R 1001:0 /opt/app-root && chmod -R og+rwx /opt/app-root - -USER 1001 - -# Set the default CMD to print the usage of the language image. -CMD $STI_SCRIPTS_PATH/usage diff --git a/3.4/README.md b/3.4/README.md deleted file mode 100644 index 8ad8e0d9..00000000 --- a/3.4/README.md +++ /dev/null @@ -1,205 +0,0 @@ -Python Docker image -=================== - -This repository contains the source for building various versions of -the Python application as a reproducible Docker image using -[source-to-image](https://github.com/openshift/source-to-image). -Users can choose between RHEL and CentOS based builder images. -The resulting image can be run using [Docker](http://docker.io). - - -Usage ---------------------- -To build a simple [python-sample-app](https://github.com/openshift/sti-python/tree/master/3.4/test/setup-test-app) application -using standalone [S2I](https://github.com/openshift/source-to-image) and then run the -resulting image with [Docker](http://docker.io) execute: - -* **For RHEL based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=3.4/test/setup-test-app/ rhscl/python-34-rhel7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -* **For CentOS based image** - ``` - $ s2i build https://github.com/openshift/sti-python.git --context-dir=3.4/test/setup-test-app/ centos/python-34-centos7 python-sample-app - $ docker run -p 8080:8080 python-sample-app - ``` - -**Accessing the application:** -``` -$ curl 127.0.0.1:8080 -``` - - -Repository organization ------------------------- -* **``** - - * **Dockerfile** - - CentOS based Dockerfile. - - * **Dockerfile.rhel7** - - RHEL based Dockerfile. In order to perform build or test actions on this - Dockerfile you need to run the action on a properly subscribed RHEL machine. - - * **`s2i/bin/`** - - This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): - - * **assemble** - - Used to install the sources into the location where the application - will be run and prepare the application for deployment (eg. installing - dependencies, etc.) - - * **run** - - This script is responsible for running the application by using the - application web server. - - * **usage*** - - This script prints the usage of this image. - - * **`contrib/`** - - This folder contains a file with commonly used modules. - - * **`test/`** - - This folder contains a [S2I](https://github.com/openshift/source-to-image) - test framework with a simple server. - - * **`setup-test-app/`** - - Simple Gunicorn application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **`standalone-test-app/`** - - Simple standalone application used for testing purposes by the [S2I](https://github.com/openshift/source-to-image) test framework. - - * **run** - - Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. - - -Environment variables ---------------------- - -To set these environment variables, you can place them as a key value pair into a `.sti/environment` -file inside your source code repository. - -* **APP_FILE** - - Used to run the application from a Python script. - This should be a path to a Python file (defaults to `app.py`) that will be - passed to the Python interpreter to start the application. - -* **APP_MODULE** - - Used to run the application with Gunicorn, as documented - [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). - This variable specifies a WSGI callable with the pattern - `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path - of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the - specified module. - Gunicorn will look for a WSGI callable named `application` if not specified. - - If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` - file in your project and use it if it exists. - - If using `setup.py` for installing the application, the `MODULE_NAME` part - can be read from there. For an example, see - [setup-test-app](https://github.com/openshift/sti-python/tree/master/3.4/test/setup-test-app). - -* **APP_CONFIG** - - Path to a valid Python file with a - [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. - -* **DISABLE_COLLECTSTATIC** - - Set this variable to a non-empty value to inhibit the execution of - 'manage.py collectstatic' during the build. This only affects Django projects. - -* **DISABLE_MIGRATE** - - Set this variable to a non-empty value to inhibit the execution of 'manage.py migrate' - when the produced image is run. This only affects Django projects. - -Source repository layout ------------------------- - -You do not need to change anything in your existing Python project's repository. -However, if these files exist they will affect the behavior of the build process: - -* **requirements.txt** - - List of dependencies to be installed with `pip`. The format is documented - [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). - - -* **setup.py** - - Configures various aspects of the project, including installation of - dependencies, as documented - [here](https://packaging.python.org/en/latest/distributing.html#setup-py). - For most projects, it is sufficient to simply use `requirements.txt`. - - -Run strategies --------------- - -The Docker image produced by sti-python executes your project in one of the -following ways, in precedence order: - -* **Gunicorn** - - The Gunicorn WSGI HTTP server is used to serve your application in the case that it - is installed. It can be installed by listing it either in the `requirements.txt` - file or in the `install_requires` section of the `setup.py` file. - - If a file named `wsgi.py` is present in your repository, it will be used as - the entry point to your application. This can be overridden with the - environment variable `APP_MODULE`. - This file is present in Django projects by default. - - If you have both Django and Gunicorn in your requirements, your Django project - will automatically be served using Gunicorn. - -* **Django development server** - - If you have Django in your requirements but don't have Gunicorn, then your - application will be served using Django's development web server. However, this is not - recommended for production environments. - -* **Python script** - - This is the most general way of executing your application. It will be used - in the case where you specify a path to a Python script via the `APP_FILE` environment - variable, defaulting to a file named `app.py` if it exists. The script is - passed to a regular Python interpreter to launch your application. - -Hot deploy ---------------------- - -If you are using Django, hot deploy will work out of the box. - -To enable hot deploy while using Gunicorn, make sure you have a Gunicorn -configuration file inside your repository with the -[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) -option set to `true`. Make sure to specify your config via the `APP_CONFIG` -environment variable. - -To change your source code in running container, use Docker's -[exec](https://docs.docker.com/reference/commandline/exec/) command: - -``` -docker exec -it /bin/bash -``` - -After you enter into the running container, your current directory is set -to `/opt/app-root/src`, where the source code is located. diff --git a/3.4/contrib/etc/generate_container_user b/3.4/contrib/etc/generate_container_user deleted file mode 100644 index 229b986a..00000000 --- a/3.4/contrib/etc/generate_container_user +++ /dev/null @@ -1,9 +0,0 @@ -# Set current user in nss_wrapper -PASSWD_DIR="/opt/app-root/etc" - -export USER_ID=$(id -u) -export GROUP_ID=$(id -g) -envsubst < ${PASSWD_DIR}/passwd.template > ${PASSWD_DIR}/passwd -export LD_PRELOAD=libnss_wrapper.so -export NSS_WRAPPER_PASSWD=${PASSWD_DIR}/passwd -export NSS_WRAPPER_GROUP=/etc/group diff --git a/3.4/contrib/etc/passwd.template b/3.4/contrib/etc/passwd.template deleted file mode 100644 index af0814d9..00000000 --- a/3.4/contrib/etc/passwd.template +++ /dev/null @@ -1,14 +0,0 @@ -root:x:0:0:root:/root:/bin/bash -bin:x:1:1:bin:/bin:/sbin/nologin -daemon:x:2:2:daemon:/sbin:/sbin/nologin -adm:x:3:4:adm:/var/adm:/sbin/nologin -lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin -sync:x:5:0:sync:/sbin:/bin/sync -shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown -halt:x:7:0:halt:/sbin:/sbin/halt -mail:x:8:12:mail:/var/spool/mail:/sbin/nologin -operator:x:11:0:operator:/root:/sbin/nologin -games:x:12:100:games:/usr/games:/sbin/nologin -ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin -nobody:x:99:99:Nobody:/:/sbin/nologin -default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/sbin/nologin diff --git a/3.4/contrib/etc/scl_enable b/3.4/contrib/etc/scl_enable deleted file mode 100644 index 6881d29a..00000000 --- a/3.4/contrib/etc/scl_enable +++ /dev/null @@ -1,5 +0,0 @@ -# IMPORTANT: Do not add more content to this file unless you know what you are -# 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 rh-python34 diff --git a/3.4/s2i/bin/assemble b/3.4/s2i/bin/assemble deleted file mode 100755 index 60ae7ff1..00000000 --- a/3.4/s2i/bin/assemble +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_collectstatic() { - is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] -} - -set -e - -echo "---> Copying application source ..." -cp -Rf /tmp/src/. ./ - -if [[ -f requirements.txt ]]; then - echo "---> Installing dependencies ..." - pip install --user -r requirements.txt -fi - -if [[ -f setup.py ]]; then - echo "---> Installing application ..." - python setup.py develop --user -fi - -if should_collectstatic; then - ( - echo "---> Collecting Django static files ..." - - # Find shallowest manage.py script, either ./manage.py or /manage.py - manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - - if [[ ! -f "$manage_file" ]]; then - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "'manage.py collectstatic' ignored." - exit - fi - - if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then - echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" - echo " $ python $manage_file collectstatic --noinput" - echo "Ignore this warning if you're not serving static files with Django." - exit - fi - - python $manage_file collectstatic --noinput - ) -fi - -# Fix source directory permissions -fix-permissions ./ -# set permissions for any installed artifacts -fix-permissions /opt/app-root - -# remove pip temporary directory -rm -rf /tmp/pip_build_default diff --git a/3.4/s2i/bin/run b/3.4/s2i/bin/run deleted file mode 100755 index 1c03566c..00000000 --- a/3.4/s2i/bin/run +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -source /opt/app-root/etc/generate_container_user - -function is_gunicorn_installed() { - hash gunicorn &>/dev/null -} - -function is_django_installed() { - python -c "import django" &>/dev/null -} - -function should_migrate() { - is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] -} - -set -e - -APP_FILE="${APP_FILE:-app.py}" -if [[ -f "$APP_FILE" ]]; then - echo "---> Running application from Python script ($APP_FILE) ..." - exec python "$APP_FILE" -fi -echo "WARNING: file '$APP_FILE' not found." - -# Find shallowest manage.py script, either ./manage.py or /manage.py -manage_file=$(find . -maxdepth 2 -type f -name 'manage.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1) - -if should_migrate; then - if [[ -f "$manage_file" ]]; then - echo "---> Migrating database ..." - python "$manage_file" migrate --noinput - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py migrate'." - fi -fi - -if is_gunicorn_installed; then - if [[ -z "$APP_MODULE" ]]; then - # Find shallowest wsgi.py file, one of ./wsgi.py, /wsgi.py or //wsgi.py, - # replace "/" with "." and remove ".py" suffix - APP_MODULE=$(find . -maxdepth 3 -type f -name 'wsgi.py' -printf '%d\t%P\n' | sort -nk1 | cut -f2 | head -1 | sed 's:/:.:;s:.py$::') - fi - - if [[ -z "$APP_MODULE" && -f setup.py ]]; then - APP_MODULE="$(python setup.py --name)" - fi - - if [[ "$APP_MODULE" ]]; then - echo "---> Serving application with gunicorn ($APP_MODULE) ..." - exec gunicorn "$APP_MODULE" --bind=0.0.0.0:8080 --access-logfile=- --config "$APP_CONFIG" - fi -fi - -if is_django_installed; then - if [[ -f "$manage_file" ]]; then - echo "---> Serving application with 'manage.py runserver' ..." - echo "WARNING: this is NOT a recommended way to run you application in production!" - echo "Consider using gunicorn or some other production web server." - exec python "$manage_file" runserver 0.0.0.0:8080 - else - echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." - echo "Skipped 'python manage.py runserver'." - fi -fi - ->&2 echo "ERROR: don't know how to run your application." ->&2 echo "Please set either APP_MODULE or APP_FILE environment variables, or create a file 'app.py' to launch your application." -exit 1 diff --git a/3.4/s2i/bin/usage b/3.4/s2i/bin/usage deleted file mode 100755 index 4ab707a9..00000000 --- a/3.4/s2i/bin/usage +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh - -DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` -NAMESPACE=centos -[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl - -cat </dev/null -} - -container_exists() { - image_exists $(cat $cid_file) -} - -container_ip() { - docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) -} - -run_s2i_build() { - info "Building the ${1} application image ..." - s2i build ${s2i_args} file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp -} - -prepare() { - if ! image_exists ${IMAGE_NAME}; then - echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." - exit 1 - fi - # TODO: S2I build require the application is a valid 'GIT' repository, we - # should remove this restriction in the future when a file:// is used. - info "Preparing to test ${1} ..." - pushd ${test_dir}/${1} >/dev/null - git init - git config user.email "build@localhost" && git config user.name "builder" - git add -A && git commit -m "Sample commit" - popd >/dev/null -} - -run_test_application() { - docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} -p ${test_port} ${IMAGE_NAME}-testapp -} - -cleanup_app() { - info "Cleaning up app container ..." - if [ -f $cid_file ]; then - if container_exists; then - docker stop $(cat $cid_file) - fi - fi -} - -cleanup() { - info "Cleaning up the test application image" - if image_exists ${IMAGE_NAME}-testapp; then - docker rmi -f ${IMAGE_NAME}-testapp - fi - rm -rf ${test_dir}/${1}/.git -} - -check_result() { - local result="$1" - if [[ "$result" != "0" ]]; then - info "TEST FAILED (${result})" - cleanup - exit $result - fi -} - -wait_for_cid() { - local max_attempts=10 - local sleep_time=1 - local attempt=1 - local result=1 - info "Waiting for application container to start $CONTAINER_ARGS ..." - while [ $attempt -le $max_attempts ]; do - [ -f $cid_file ] && [ -s $cid_file ] && break - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done -} - -test_s2i_usage() { - info "Testing 's2i usage' ..." - s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null -} - -test_docker_run_usage() { - info "Testing 'docker run' usage ..." - docker run ${IMAGE_NAME} &>/dev/null -} - -test_scl_usage() { - local run_cmd="$1" - local expected="$2" - local cid_file="$3" - - info "Testing the image SCL enable" - out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi - out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) - if ! echo "${out}" | grep -q "${expected}"; then - echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" - return 1 - fi -} - -test_connection() { - info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." - local max_attempts=30 - local sleep_time=1 - local attempt=1 - local result=1 - while [ $attempt -le $max_attempts ]; do - response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) - status=$? - if [ $status -eq 0 ]; then - if [ $response_code -eq 200 ]; then - result=0 - fi - break - fi - attempt=$(( $attempt + 1 )) - sleep $sleep_time - done - return $result -} - -test_application() { - local cid_file=$(mktemp -u --suffix=.cid) - # Verify that the HTTP connection can be established to test application container - run_test_application & - - # Wait for the container to write it's CID file - wait_for_cid - - test_scl_usage "python --version" "Python 3.4.2" "${cid_file}" - check_result $? - test_connection - check_result $? - cleanup_app -} - - -# Since we built the candidate image locally, we don't want S2I attempt to pull -# it from Docker hub -s2i_args="--force-pull=false" - -# Verify the 'usage' script is working properly when running the base image with 's2i usage ...' -test_s2i_usage -check_result $? - -# Verify the 'usage' script is working properly when running the base image with 'docker run ...' -test_docker_run_usage -check_result $? - -for app in ${WEB_APPS[@]}; do - prepare ${app} - run_s2i_build ${app} - check_result $? - - # test application with default user - test_application - - # test application with random user - CONTAINER_ARGS="-u 12345" test_application - - info "All tests for the ${app} finished successfully." - cleanup ${app} -done - -info "All tests finished successfully." diff --git a/3.4/test/standalone-test-app/app.py b/3.4/test/standalone-test-app/app.py deleted file mode 100644 index d786ee79..00000000 --- a/3.4/test/standalone-test-app/app.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -from gunicorn.app.base import BaseApplication -from gunicorn.six import iteritems - - -def wsgi_handler(environ, start_response): - start_response('200 OK', [('Content-Type','text/html')]) - return [b"Hello World from standalone WSGI application!"] - -class StandaloneApplication(BaseApplication): - def __init__(self, app, options=None): - self.options = options or {} - self.application = app - super(StandaloneApplication, self).__init__() - - def load_config(self): - config = dict([(key, value) for key, value in iteritems(self.options) - if key in self.cfg.settings and value is not None]) - for key, value in iteritems(config): - self.cfg.set(key.lower(), value) - - def load(self): - return self.application - -if __name__ == '__main__': - StandaloneApplication(wsgi_handler, {'bind': ':8080'}).run() diff --git a/3.4/test/standalone-test-app/requirements.txt b/3.4/test/standalone-test-app/requirements.txt deleted file mode 100644 index 8f22dccf..00000000 --- a/3.4/test/standalone-test-app/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -gunicorn diff --git a/3.6/Dockerfile.rhel8 b/3.6/Dockerfile.rhel8 new file mode 100644 index 00000000..4ea41730 --- /dev/null +++ b/3.6/Dockerfile.rhel8 @@ -0,0 +1,81 @@ +# This image provides a Python 3.6 environment you can use to run your Python +# applications. +FROM ubi8/s2i-base:1 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.6 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi8-python-36 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.6" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python36,python-36,rh-python36" \ + com.redhat.component="python-36-container" \ + name="ubi8/python-36" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.6/test/setup-test-app/ ubi8/python-36 python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi8-python-36" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python36 python36-devel python3-virtualenv python3-setuptools python3-pip \ + nss_wrapper-libs httpd httpd-devel mod_ssl mod_auth_gssapi \ + mod_ldap mod_session atlas-devel gcc-gfortran libffi-devel \ + libtool-ltdl enchant krb5-devel" && \ + yum -y module enable python36:3.6 httpd:2.4 && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + # Remove redhat-logos-httpd (httpd dependency) to keep image size smaller. + rpm -e --nodeps redhat-logos-httpd && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.6/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.6/root/ / + +# - 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 \ + virtualenv-$PYTHON_VERSION ${APP_ROOT} && \ + rm -r /opt/wheels && \ + chown -R 1001:0 ${APP_ROOT} && \ + fix-permissions ${APP_ROOT} -P && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.6/README.md b/3.6/README.md new file mode 100644 index 00000000..cb418efb --- /dev/null +++ b/3.6/README.md @@ -0,0 +1,388 @@ +Python 3.6 container image +========================= + +This container image includes Python 3.6 as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.6 applications. +Users can choose between RHEL, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +Description +----------- + +Python 3.6 available as container is a base platform for +building and running various Python 3.6 applications and frameworks. +Python is an easy to learn, powerful programming language. It has efficient high-level +data structures and a simple but effective approach to object-oriented programming. +Python's elegant syntax and dynamic typing, together with its interpreted nature, +make it an ideal language for scripting and rapid application development in many areas +on most platforms. + +This container image includes an npm utility +(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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:3.6-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:3.6-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull registry.access.redhat.com/ubi8/python-36 +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM registry.access.redhat.com/ubi8/python-36 + +# 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 +``` + +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: + +``` +FROM registry.access.redhat.com/ubi8/python-36 + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.6/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/3.6/root/opt/app-root/etc/generate_container_user b/3.6/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.6/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.6/root/opt/wheels b/3.6/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.6/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.6/s2i/bin/assemble b/3.6/s2i/bin/assemble new file mode 100755 index 00000000..a2206292 --- /dev/null +++ b/3.6/s2i/bin/assemble @@ -0,0 +1,147 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_collectstatic() { + is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] +} + +function virtualenv_bin() { + if head "/etc/redhat-release" | grep -q "^CentOS Linux release 7" || \ + head "/etc/redhat-release" | grep -q "^Red Hat Enterprise Linux\( Server\)\? release 7" || \ + head "/etc/redhat-release" | grep -q "^Fedora release"; then + virtualenv $1 + else + virtualenv-${PYTHON_VERSION} $1 + fi +} + +# 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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + +# Python <3.7 only code, Python 3.7+ uses bundled wheel from Fedora +# We have to first upgrade pip to at least 19.3 because +# pip < 19.3 does not support manylinux2014 wheels. Only manylinux2014 (and later) wheels +# support platforms like ppc64le, aarch64 or armv7 +echo "---> Upgrading pip to latest version supported for Python $PYTHON_VERSION ..." +if ! pip install -U "pip"; then + echo "WARNING: Installation of 'pip' failed, trying again from official PyPI with pip --isolated install" + pip install --isolated -U "pip" +fi + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.6/s2i/bin/init-wrapper b/3.6/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.6/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/3.6/s2i/bin/run b/3.6/s2i/bin/run new file mode 100755 index 00000000..7003f038 --- /dev/null +++ b/3.6/s2i/bin/run @@ -0,0 +1,155 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.6/s2i/bin/usage b/3.6/s2i/bin/usage new file mode 100755 index 00000000..0d2e8ef0 --- /dev/null +++ b/3.6/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.6/test/__init__.py b/3.6/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.6/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.6/test/app-home-test-app b/3.6/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.6/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.6/test/app-module-test-app b/3.6/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.6/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.6/test/check_imagestreams.py b/3.6/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.6/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.6/test/conftest.py b/3.6/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.6/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.6/test/django-different-port-test-app b/3.6/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.6/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.6/test/django-test-app b/3.6/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.6/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.6/test/from-dockerfile/Dockerfile.tpl b/3.6/test/from-dockerfile/Dockerfile.tpl new file mode 120000 index 00000000..ba88ccc4 --- /dev/null +++ b/3.6/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/Dockerfile.tpl \ No newline at end of file diff --git a/3.6/test/from-dockerfile/Dockerfile_no_s2i.tpl b/3.6/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/3.6/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/3.6/test/gunicorn-config-different-port-test-app b/3.6/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.6/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.6/test/gunicorn-different-port-test-app b/3.6/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.6/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.6/test/gunicorn-python-configfile-different-port-test-app b/3.6/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.6/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.6/test/imagestreams b/3.6/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.6/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.6/test/locale-test-app b/3.6/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.6/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.6/test/micropipenv-requirements-test-app b/3.6/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.6/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.6/test/micropipenv-test-app/.gitignore b/3.6/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.6/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/3.6/test/micropipenv-test-app/.s2i/environment b/3.6/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.6/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/3.6/test/micropipenv-test-app/Pipfile b/3.6/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..fff4732e --- /dev/null +++ b/3.6/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.6" diff --git a/3.6/test/micropipenv-test-app/Pipfile.lock b/3.6/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..9102b17b --- /dev/null +++ b/3.6/test/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,163 @@ +{ + "_meta": { + "hash": { + "sha256": "3cc443e101d93694814e3e48b272fafa4dc1efb36de327254b32c6683afc5b47" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "gunicorn": { + "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", + "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "markers": "python_version >= '3.5'", + "version": "==20.1.0" + }, + "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:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8' and python_version < '3.8'", + "version": "==4.0.1" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" + }, + "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:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", + "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + ], + "index": "pypi", + "version": "==6.2.3" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + } + } +} diff --git a/3.6/test/micropipenv-test-app/setup.py b/3.6/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.6/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/3.6/test/micropipenv-test-app/testapp.py b/3.6/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.6/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.6/test/mod-wsgi-test-app b/3.6/test/mod-wsgi-test-app new file mode 120000 index 00000000..9d8c3338 --- /dev/null +++ b/3.6/test/mod-wsgi-test-app @@ -0,0 +1 @@ +../../examples/mod-wsgi-test-app \ No newline at end of file diff --git a/3.6/test/npm-virtualenv-uwsgi-test-app b/3.6/test/npm-virtualenv-uwsgi-test-app new file mode 120000 index 00000000..b34ae21e --- /dev/null +++ b/3.6/test/npm-virtualenv-uwsgi-test-app @@ -0,0 +1 @@ +../../examples/npm-virtualenv-uwsgi-test-app \ No newline at end of file diff --git a/3.6/test/numpy-test-app b/3.6/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.6/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.6/test/pin-pipenv-version-test-app/.s2i/environment b/3.6/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..9bfe6709 --- /dev/null +++ b/3.6/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2021.5.29 diff --git a/3.6/test/pin-pipenv-version-test-app/app.sh b/3.6/test/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/3.6/test/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.6/test/pin-pipenv-version-test-app/requirements.txt b/3.6/test/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/3.6/test/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/3.6/test/pin-pipenv-version-test-app/wsgi.py b/3.6/test/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.6/test/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.6/test/pipenv-and-micropipenv-should-fail-test-app b/3.6/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.6/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.6/test/pipenv-test-app/.gitignore b/3.6/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.6/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/3.6/test/pipenv-test-app/.s2i/environment b/3.6/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.6/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.6/test/pipenv-test-app/Pipfile b/3.6/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..fff4732e --- /dev/null +++ b/3.6/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.6" diff --git a/3.6/test/pipenv-test-app/Pipfile.lock b/3.6/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..9102b17b --- /dev/null +++ b/3.6/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,163 @@ +{ + "_meta": { + "hash": { + "sha256": "3cc443e101d93694814e3e48b272fafa4dc1efb36de327254b32c6683afc5b47" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "gunicorn": { + "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", + "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "markers": "python_version >= '3.5'", + "version": "==20.1.0" + }, + "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:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8' and python_version < '3.8'", + "version": "==4.0.1" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" + }, + "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:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", + "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + ], + "index": "pypi", + "version": "==6.2.3" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + } + } +} diff --git a/3.6/test/pipenv-test-app/setup.py b/3.6/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.6/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/3.6/test/pipenv-test-app/testapp.py b/3.6/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.6/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.6/test/poetry-src-layout-test-app b/3.6/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.6/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.6/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.6/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..9bfe6709 --- /dev/null +++ b/3.6/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2021.5.29 diff --git a/3.6/test/pyuwsgi-pipenv-test-app/app.sh b/3.6/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.6/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.6/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.6/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.6/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.6/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.6/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.6/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.6/test/run b/3.6/test/run new file mode 100755 index 00000000..6793f5f8 --- /dev/null +++ b/3.6/test/run @@ -0,0 +1,311 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + django_example_repo_url="https://github.com/sclorg/django-ex.git@2.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.6/test/run-openshift-pytest b/3.6/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.6/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.6/test/run-pytest b/3.6/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.6/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.6/test/setup-cfg-test-app b/3.6/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.6/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.6/test/setup-requirements-test-app b/3.6/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.6/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.6/test/setup-test-app b/3.6/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.6/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.6/test/standalone-custom-pypi-index-test-app b/3.6/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.6/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.6/test/standalone-test-app b/3.6/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.6/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.6/test/test-lib.sh b/3.6/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.6/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.6/test/test_container_application.py b/3.6/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.6/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.6/test/test_container_basics.py b/3.6/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.6/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.6/test/test_ocp_deploy_templates.py b/3.6/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.6/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.6/test/test_ocp_helm_python_django_app.py b/3.6/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.6/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.6/test/test_ocp_helm_python_django_psql_persistent.py b/3.6/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.6/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.6/test/test_ocp_helm_python_imagestreams.py b/3.6/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.6/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.6/test/test_ocp_imagestreams_quickstart.py b/3.6/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.6/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.6/test/test_ocp_python_ex_standalone.py b/3.6/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.6/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.6/test/test_ocp_python_ex_template.py b/3.6/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.6/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.9-minimal/Dockerfile.c9s b/3.9-minimal/Dockerfile.c9s new file mode 100644 index 00000000..f5f2d5fc --- /dev/null +++ b/3.9-minimal/Dockerfile.c9s @@ -0,0 +1,104 @@ +FROM quay.io/centos/centos:stream9-minimal + + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.9 \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-39 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el9" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.9" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python39,python-39,rh-python39" \ + com.redhat.component="python-39-container" \ + name="sclorg/python-39-minimal-c9s" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.9-minimal/test/setup-test-app/ ubi9/python-39-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-39-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="python39 glibc-langpack-en findutils nss_wrapper-libs" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.9-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.9-minimal/root/ / + +# Python 3.7+ 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-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.9-minimal/README.md b/3.9-minimal/README.md new file mode 100644 index 00000000..11a225e4 --- /dev/null +++ b/3.9-minimal/README.md @@ -0,0 +1,125 @@ +Python 3.9 container image - minimal version +============================================ + +This container image is a special version of the [full Python 3.9 container image](https://github.com/sclorg/s2i-python-container/tree/master/3.9) +provided as a [S2I](https://github.com/openshift/source-to-image) base image for your Python 3.9 applications. + +Because the minimal and full images work similarly, we document here only the differences and limitations +of the minimal container image. For the documentation of common features see the [full container image docs](https://github.com/sclorg/s2i-python-container/tree/master/3.9). + +The Python 3.9 minimal container image is currently considered a tech-preview and only available on quay.io. +The image is built on top of the [official CentOS Stream base containers](quay.io/centos/centos). + +To pull the Python 3.9 minimal container image to build on, run + +``` +podman pull quay.io/sclorg/python-39-minimal-c9s +``` + +Description +----------- + +The full container image is a universal base image to build your containerized applications on top of. However, its universal nature +means that the resulting containers it produces consume a lot of disk space. This is caused mainly by the fact that the image contains +npm, compilers, header files and some other packages one might need to install and deploy their applications. + +Because size does matter for us and our customers, we have prepared this minimal container image with very limited subset +of installed packages. There are no compilers, no header files, no npm etc and the yum package manager is replaced with a minimalistic +reimplementation called microdnf, making the resulting container images much smaller. This creates some limitations +but we provide ways to workaround them. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Limitations +----------- + +1. There is only a very limited subset of packages installed. They are choosen carefully to satisfy most of the Python apps but your app might have some special needs. +1. There is no npm and nodejs. +1. There are no compilers and header files. Installation from Python wheels should still work but compilation from a source code is not supported out of the box. + +In the next chapter, we provide three possible workarounds for the mentioned limitations of the minimal container image. + +Possible solutions for the limitations +-------------------------------------- + +### Use the full container image + +It's easy at that. If you don't want to write your own Dockerfile and disk space is not a problem, use +the full universal container image and you should be fine. + +### Build your own container image on top of the minimal container image + +Let's say that your application depends on uwsgi. uwsgi cannot be installed from Python wheel and has to be +compiled from source which requires some additional packages to be installed - namely gcc for the compilation +itself and python39-devel containing Python header files. + +To solve that problem, you can use all the pieces provided by the minimal container image and just add one more +step to install the missing dependencies: + +``` +FROM python-39-minimal + +# 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 + +# Install packages necessary for compiling uwsgi from source +RUN microdnf install -y gcc python39-devel +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +If you do it this way, your problem with the missing packages is solved. But there is also one disadvantage: the resulting +runtime image contains unnecessary compiler and Python header files. How to solve this? Uninstalling them at the end +of the Dockerfile is not really a solution but we have one. Keep reading. + +### Build on full image, run on minimal image + +Did you know that you can copy files from one image to another one during a build? That's the feature we are gonna use now. +We use the full container image with all compilers and other usefull packages installed to build our app and its dependencies +and we then move the result including the whole virtual environemnt to the minimal container image. + +This app needs mod_wsgi and to install (compile it from source) it, we'll need: httpd-devel for header files, gcc and redhat-rpm-config +as a compiler and configuratuion and finally python39-devel containing Python header files. There is no need to install those packages +manually because the full container image already contains them. However, the application needs httpd as a runtime dependency +so we need to install it to the minimal container image as well. + +``` +# Part 1 - build + +FROM python-39 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +# Part 2 - deploy + +FROM python-39-minimal + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd package - runtime dependency of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +This way, the resulting container image does contain only necessary dependencies and it's much lighter. diff --git a/3.9-minimal/root/opt/app-root/etc/generate_container_user b/3.9-minimal/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.9-minimal/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.9-minimal/root/opt/wheels b/3.9-minimal/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.9-minimal/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.9-minimal/root/usr/bin/cgroup-limits b/3.9-minimal/root/usr/bin/cgroup-limits new file mode 100755 index 00000000..6fa19153 --- /dev/null +++ b/3.9-minimal/root/usr/bin/cgroup-limits @@ -0,0 +1,192 @@ +#!/usr/libexec/platform-python + +""" +Script for parsing cgroup information + +This script will read some limits from the cgroup system and parse +them, printing out "VARIABLE=VALUE" on each line for every limit that is +successfully read. Output of this script can be directly fed into +bash's export command. Recommended usage from a bash script: + + set -o errexit + export_vars=$(cgroup-limits) ; export $export_vars + +Variables currently supported: + MAX_MEMORY_LIMIT_IN_BYTES + Maximum possible limit MEMORY_LIMIT_IN_BYTES can have. This is + currently constant value of 9223372036854775807. + MEMORY_LIMIT_IN_BYTES + Maximum amount of user memory in bytes. If this value is set + to the same value as MAX_MEMORY_LIMIT_IN_BYTES, it means that + there is no limit set. The value is taken from + /sys/fs/cgroup/memory/memory.limit_in_bytes for cgroups v1 + and from /sys/fs/cgroup/memory.max for cgroups v2 + NUMBER_OF_CORES + Number of CPU cores that can be used. If both the cpu and cpuset + controllers specify a limit, the controller with the lowest CPU + limit takes precedence. For the cpu controller, the value is + calculated from /sys/fs/cgroup/cpu/cpu.cfs_{quota,period}_us for + cgroups v1 and from /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2. + For the cpuset controller, the value is taken from + /sys/fs/cgroup/cpuset/cpuset.cpus for cgroups v1 and from + /sys/fs/cgroup/cpuset.cpus.effective for cgroups v2 + NO_MEMORY_LIMIT + Set to "true" if MEMORY_LIMIT_IN_BYTES is so high that the caller + can act as if no memory limit was set. Undefined otherwise. + +Note about non-root containers: + + Per podman-run(1) man page, on some systems, changing the resource limits + may not be allowed for non-root users. For more details, see + https://github.com/containers/podman/blob/main/troubleshooting.md#26-running-containers-with-resource-limits-fails-with-a-permissions-error. + +How to test this script: + + Run a container as root and see whether the output of available memory + and CPUs match what is either available on the host or specified via + cgroups limits by the container runtime (podman). + + For example: + + # This should return NO_MEMORY_LIMIT=true, NUMBER_OF_CORES= + sudo podman run -ti --rm quay.io/fedora/s2i-core /usr/bin/cgroup-limits + + # This should return MEMORY_LIMIT_IN_BYTES=2147483648, NUMBER_OF_CORES=3 + # 3 CPUs despite --cpus 4 was given is correct, because the cpuset 2-4 means + # running on 3 concrete processors only, which is lower limit than --cpus + # and thus is preferred + sudo podman run -ti --rm --cpus 4 --cpuset-cpus=2-4 --memory 2G quay.io/fedora/s2i-core /usr/bin/cgroup-limits +""" + +from __future__ import division +from __future__ import print_function +import sys +import subprocess + + +def _read_file(path): + try: + with open(path, 'r') as f: + return f.read().strip() + except IOError: + return None + + +def get_memory_limit(): + """ + Read memory limit, in bytes. + """ + + limit = _read_file('/sys/fs/cgroup/memory/memory.limit_in_bytes') + # If first file does not exist, try cgroups v2 file + limit = limit or _read_file('/sys/fs/cgroup/memory.max') + if limit is None or not limit.isdigit(): + if limit == 'max': + return 9223372036854775807 + print("Warning: Can't detect memory limit from cgroups", + file=sys.stderr) + return None + return int(limit) + + +def get_number_of_cores(): + """ + Read number of CPU cores. + """ + + limits = [l for l in [_read_cpu_quota(), _read_cpuset_size(), _read_nproc()] if l] + if not limits: + return None + return min(limits) + + +def _read_cpu_quota(): + + quota, period = None, None + + quota = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_quota_us") + if quota: + # cgroups v1 + quota = quota.strip() + if quota == "-1": + return None + + period = _read_file("/sys/fs/cgroup/cpu/cpu.cfs_period_us") + if period: + period = period.strip() + + else: + # cgroups v2 + line = _read_file("/sys/fs/cgroup/cpu.max") + if line: + fields = line.split() + + if len(fields) >= 2: + quota = fields[0] + if quota == "max": + return None + period = fields[1] + + + if quota and quota.isdigit() and period and period.isdigit(): + return int(quota)//int(period) + + print("Warning: Can't detect cpu quota from cgroups", + file=sys.stderr) + return None + + +def _read_cpuset_size(): + + core_count = 0 + + line = _read_file('/sys/fs/cgroup/cpuset/cpuset.cpus') + # If first file does not exist, try cgroups v2 file + line = line or _read_file('/sys/fs/cgroup/cpuset.cpus.effective') + if line is None: + # None of the files above exists when running podman as non-root, + # so in that case, this warning is printed every-time + print("Warning: Can't detect cpuset size from cgroups, will use nproc", + file=sys.stderr) + return None + + for group in line.split(','): + core_ids = list(map(int, group.split('-'))) + if len(core_ids) == 2: + core_count += core_ids[1] - core_ids[0] + 1 + else: + core_count += 1 + + return core_count + + +def _read_nproc(): + """ + Returns number of cores without looking at cgroup limits. + This might work as the last resort when running a container as non-root + where cgroups v2 resource limits cannot be set without a specific + configuration (per podman-run(1) man page). + """ + try: + stdout, stderr = subprocess.Popen('nproc', stdout=subprocess.PIPE).communicate() + return int(stdout) + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + return None + + +if __name__ == "__main__": + env_vars = { + "MAX_MEMORY_LIMIT_IN_BYTES": 9223372036854775807, + "MEMORY_LIMIT_IN_BYTES": get_memory_limit(), + "NUMBER_OF_CORES": get_number_of_cores() + } + + env_vars = {k: v for k, v in env_vars.items() if v is not None} + + if env_vars.get("MEMORY_LIMIT_IN_BYTES", 0) >= 92233720368547: + env_vars["NO_MEMORY_LIMIT"] = "true" + + for key, value in env_vars.items(): + print("{0}={1}".format(key, value)) diff --git a/3.9-minimal/root/usr/bin/fix-permissions b/3.9-minimal/root/usr/bin/fix-permissions new file mode 100755 index 00000000..ddd33ace --- /dev/null +++ b/3.9-minimal/root/usr/bin/fix-permissions @@ -0,0 +1,27 @@ +#!/bin/sh + +# Allow this script to fail without failing a build +set +e + +SYMLINK_OPT=${2:--L} + +# Fix permissions on the given directory or file to allow group read/write of +# regular files and execute of directories. + +[ $(id -u) -ne 0 ] && CHECK_OWNER=" -uid $(id -u)" + +# If argument does not exist, script will still exit with 0, +# but at least we'll see something went wrong in the log +if ! [ -e "$1" ] ; then + echo "ERROR: File or directory $1 does not exist." >&2 + # We still want to end successfully + exit 0 +fi + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -gid 0 -exec chgrp 0 {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} \! -perm -g+rw -exec chmod g+rw {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -perm /u+x -a \! -perm /g+x -exec chmod g+x {} + +find $SYMLINK_OPT "$1" ${CHECK_OWNER} -type d \! -perm /g+x -exec chmod g+x {} + + +# Always end successfully +exit 0 diff --git a/3.9-minimal/root/usr/bin/rpm-file-permissions b/3.9-minimal/root/usr/bin/rpm-file-permissions new file mode 100755 index 00000000..8be1fb0d --- /dev/null +++ b/3.9-minimal/root/usr/bin/rpm-file-permissions @@ -0,0 +1,21 @@ +#!/bin/sh + +CHECK_DIRS="/ /opt /etc /usr /usr/bin /usr/lib /usr/lib64 /usr/share /usr/libexec" + +rpm_format="[%{FILESTATES:fstate} %7{FILEMODES:octal} %{FILENAMES:shescape}\n]" + +rpm -q --qf "$rpm_format" filesystem | while read line +do + eval "set -- $line" + + case $1 in + normal) ;; + *) continue ;; + esac + + case " $CHECK_DIRS " in + *" $3 "*) + chmod "${2: -4}" "$3" + ;; + esac +done diff --git a/3.9-minimal/s2i/bin/assemble b/3.9-minimal/s2i/bin/assemble new file mode 100755 index 00000000..bbe66b8f --- /dev/null +++ b/3.9-minimal/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.9-minimal/s2i/bin/init-wrapper b/3.9-minimal/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/s2i/bin/run b/3.9-minimal/s2i/bin/run new file mode 100755 index 00000000..bc0b3a15 --- /dev/null +++ b/3.9-minimal/s2i/bin/run @@ -0,0 +1,157 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + # Using python and just python here because the script has + # platform-python in its shebang which we don't have in the minimal image + limit_vars=$(python /usr/bin/cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.9-minimal/s2i/bin/usage b/3.9-minimal/s2i/bin/usage new file mode 100755 index 00000000..9df53ec5 --- /dev/null +++ b/3.9-minimal/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.9-minimal/test/__init__.py b/3.9-minimal/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.9-minimal/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.9-minimal/test/app-home-test-app b/3.9-minimal/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.9-minimal/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.9-minimal/test/app-module-test-app b/3.9-minimal/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.9-minimal/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.9-minimal/test/check_imagestreams.py b/3.9-minimal/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.9-minimal/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.9-minimal/test/conftest.py b/3.9-minimal/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.9-minimal/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.9-minimal/test/django-different-port-test-app b/3.9-minimal/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.9-minimal/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.9-minimal/test/django-test-app b/3.9-minimal/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.9-minimal/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.9-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl b/3.9-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl new file mode 120000 index 00000000..f5c5b907 --- /dev/null +++ b/3.9-minimal/test/from-dockerfile/mod_wsgi.Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl \ No newline at end of file diff --git a/3.9-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl b/3.9-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl new file mode 100644 index 00000000..69defb98 --- /dev/null +++ b/3.9-minimal/test/from-dockerfile/uwsgi.Dockerfile.tpl @@ -0,0 +1,20 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +ENV UPGRADE_PIP_TO_LATEST=1 + +# 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 +# Install packages necessary for compiling uwsgi from source +# pkgconfig(python-3.9) is provided by both python3-devel in c9s +# and python39-devel in UBI8. +RUN microdnf install -y gcc "pkgconfig(python-3.9)" which +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/3.9-minimal/test/gunicorn-config-different-port-test-app b/3.9-minimal/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.9-minimal/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.9-minimal/test/gunicorn-different-port-test-app b/3.9-minimal/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.9-minimal/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.9-minimal/test/gunicorn-python-configfile-different-port-test-app b/3.9-minimal/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.9-minimal/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.9-minimal/test/imagestreams b/3.9-minimal/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.9-minimal/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.9-minimal/test/locale-test-app b/3.9-minimal/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.9-minimal/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.9-minimal/test/micropipenv-requirements-test-app b/3.9-minimal/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.9-minimal/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.9-minimal/test/micropipenv-test-app/.gitignore b/3.9-minimal/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/micropipenv-test-app/.s2i/environment b/3.9-minimal/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/micropipenv-test-app/Pipfile b/3.9-minimal/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..117b223d --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/micropipenv-test-app/Pipfile.lock b/3.9-minimal/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..25741732 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/micropipenv-test-app/setup.py b/3.9-minimal/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/micropipenv-test-app/testapp.py b/3.9-minimal/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.9-minimal/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.9-minimal/test/numpy-test-app b/3.9-minimal/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.9-minimal/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.9-minimal/test/pipenv-and-micropipenv-should-fail-test-app b/3.9-minimal/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.9-minimal/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.9-minimal/test/pipenv-test-app/.gitignore b/3.9-minimal/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/pipenv-test-app/.s2i/environment b/3.9-minimal/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.9-minimal/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.9-minimal/test/pipenv-test-app/Pipfile b/3.9-minimal/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..117b223d --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/pipenv-test-app/Pipfile.lock b/3.9-minimal/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..25741732 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/pipenv-test-app/setup.py b/3.9-minimal/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.9-minimal/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/3.9-minimal/test/pipenv-test-app/testapp.py b/3.9-minimal/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.9-minimal/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.9-minimal/test/poetry-src-layout-test-app b/3.9-minimal/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.9-minimal/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.9-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.9-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..9bfe6709 --- /dev/null +++ b/3.9-minimal/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2021.5.29 diff --git a/3.9-minimal/test/pyuwsgi-pipenv-test-app/app.sh b/3.9-minimal/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.9-minimal/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.9-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.9-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.9-minimal/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.9-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.9-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.9-minimal/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.9-minimal/test/run b/3.9-minimal/test/run new file mode 100755 index 00000000..8de24fe3 --- /dev/null +++ b/3.9-minimal/test/run @@ -0,0 +1,308 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${MINIMAL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile_minimal +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.9-minimal/test/run-openshift-pytest b/3.9-minimal/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.9-minimal/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.9-minimal/test/run-pytest b/3.9-minimal/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.9-minimal/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.9-minimal/test/setup-cfg-test-app b/3.9-minimal/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.9-minimal/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.9-minimal/test/setup-requirements-test-app b/3.9-minimal/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.9-minimal/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.9-minimal/test/setup-test-app b/3.9-minimal/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.9-minimal/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.9-minimal/test/standalone-custom-pypi-index-test-app b/3.9-minimal/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.9-minimal/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.9-minimal/test/standalone-test-app b/3.9-minimal/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.9-minimal/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.9-minimal/test/test-lib.sh b/3.9-minimal/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.9-minimal/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.9-minimal/test/test_container_application.py b/3.9-minimal/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.9-minimal/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.9-minimal/test/test_container_basics.py b/3.9-minimal/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.9-minimal/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_deploy_templates.py b/3.9-minimal/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.9-minimal/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_helm_python_django_app.py b/3.9-minimal/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.9-minimal/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_helm_python_django_psql_persistent.py b/3.9-minimal/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.9-minimal/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_helm_python_imagestreams.py b/3.9-minimal/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.9-minimal/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_imagestreams_quickstart.py b/3.9-minimal/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.9-minimal/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_python_ex_standalone.py b/3.9-minimal/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.9-minimal/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.9-minimal/test/test_ocp_python_ex_template.py b/3.9-minimal/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.9-minimal/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/3.9-minimal/test/uwsgi-test-app b/3.9-minimal/test/uwsgi-test-app new file mode 120000 index 00000000..c3ccf241 --- /dev/null +++ b/3.9-minimal/test/uwsgi-test-app @@ -0,0 +1 @@ +../../examples/uwsgi-test-app \ No newline at end of file diff --git a/3.9/Dockerfile.c9s b/3.9/Dockerfile.c9s new file mode 100644 index 00000000..c3475d9f --- /dev/null +++ b/3.9/Dockerfile.c9s @@ -0,0 +1,89 @@ +# This image provides a Python 3.9 environment you can use to run your Python +# applications. +FROM quay.io/sclorg/s2i-base-c9s:c9s + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.9 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + PIP_NO_CACHE_DIR=off + +ENV NAME=python3 \ + ARCH=x86_64 + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.9" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python39,python-39,rh-python39" \ + com.redhat.component="$NAME" \ + name="sclorg/python-39-c9s" \ + 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 nss_wrapper-libs \ + httpd httpd-devel mod_ssl mod_auth_gssapi mod_ldap \ + mod_session atlas-devel gcc-gfortran libffi-devel libtool-ltdl \ + enchant krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.9/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.9/root/ / + +# Python 3.7+ 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.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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 && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.9/Dockerfile.rhel9 b/3.9/Dockerfile.rhel9 new file mode 100644 index 00000000..9fd315d4 --- /dev/null +++ b/3.9/Dockerfile.rhel9 @@ -0,0 +1,95 @@ +# This image provides a Python 3.9 environment you can use to run your Python +# applications. +FROM ubi9/s2i-base:1 + +EXPOSE 8080 + +ENV PYTHON_VERSION=3.9 \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi9-python-39 \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off + +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python 3.9" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python39,python-39,rh-python39" \ + com.redhat.component="python-39-container" \ + name="ubi9/python-39" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir=3.9/test/setup-test-app/ ubi9/python-39 python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi9-python-39" \ + maintainer="SoftwareCollections.org " + +RUN INSTALL_PKGS="python3 python3-devel python3-setuptools python3-pip nss_wrapper-libs \ + httpd httpd-devel mod_ssl mod_auth_gssapi mod_ldap \ + mod_session atlas-devel gcc-gfortran libffi-devel libtool-ltdl \ + enchant krb5-devel" && \ + yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + # Remove redhat-logos-httpd (httpd dependency) to keep image size smaller. + rpm -e --nodeps redhat-logos-httpd && \ + yum -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY 3.9/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY 3.9/root/ / + +# Python 3.7+ 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.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ + # We have to upgrade pip to a newer verison because \ + # 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 && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/3.9/README.md b/3.9/README.md new file mode 100644 index 00000000..e952d650 --- /dev/null +++ b/3.9/README.md @@ -0,0 +1,388 @@ +Python 3.9 container 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, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:3.9-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:3.9-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull registry.access.redhat.com/ubi9/python-39 +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM registry.access.redhat.com/ubi9/python-39 + +# 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 +``` + +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: + +``` +FROM registry.access.redhat.com/ubi9/python-39 + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/3.9/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/3.9/root/opt/app-root/etc/generate_container_user b/3.9/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/3.9/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/3.9/root/opt/wheels b/3.9/root/opt/wheels new file mode 120000 index 00000000..4eabc067 --- /dev/null +++ b/3.9/root/opt/wheels @@ -0,0 +1 @@ +../../../src/root/opt/wheels/ \ No newline at end of file diff --git a/3.9/s2i/bin/assemble b/3.9/s2i/bin/assemble new file mode 100755 index 00000000..bbe66b8f --- /dev/null +++ b/3.9/s2i/bin/assemble @@ -0,0 +1,134 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/3.9/s2i/bin/init-wrapper b/3.9/s2i/bin/init-wrapper new file mode 100755 index 00000000..26c8767d --- /dev/null +++ b/3.9/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/3.9/s2i/bin/run b/3.9/s2i/bin/run new file mode 100755 index 00000000..7003f038 --- /dev/null +++ b/3.9/s2i/bin/run @@ -0,0 +1,155 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + limit_vars=$(cgroup-limits) + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/3.9/s2i/bin/usage b/3.9/s2i/bin/usage new file mode 100755 index 00000000..a45027f0 --- /dev/null +++ b/3.9/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/3.9/test/__init__.py b/3.9/test/__init__.py new file mode 120000 index 00000000..d0f4746a --- /dev/null +++ b/3.9/test/__init__.py @@ -0,0 +1 @@ +../../test/__init__.py \ No newline at end of file diff --git a/3.9/test/app-home-test-app b/3.9/test/app-home-test-app new file mode 120000 index 00000000..f6b42e77 --- /dev/null +++ b/3.9/test/app-home-test-app @@ -0,0 +1 @@ +../../examples/app-home-test-app \ No newline at end of file diff --git a/3.9/test/app-module-test-app b/3.9/test/app-module-test-app new file mode 120000 index 00000000..fa20034b --- /dev/null +++ b/3.9/test/app-module-test-app @@ -0,0 +1 @@ +../../examples/app-module-test-app \ No newline at end of file diff --git a/3.9/test/check_imagestreams.py b/3.9/test/check_imagestreams.py new file mode 120000 index 00000000..56bb2be7 --- /dev/null +++ b/3.9/test/check_imagestreams.py @@ -0,0 +1 @@ +../../common/check_imagestreams.py \ No newline at end of file diff --git a/3.9/test/conftest.py b/3.9/test/conftest.py new file mode 120000 index 00000000..3f2d7840 --- /dev/null +++ b/3.9/test/conftest.py @@ -0,0 +1 @@ +../../test/conftest.py \ No newline at end of file diff --git a/3.9/test/django-different-port-test-app b/3.9/test/django-different-port-test-app new file mode 120000 index 00000000..cc115af1 --- /dev/null +++ b/3.9/test/django-different-port-test-app @@ -0,0 +1 @@ +../../examples/django-different-port-test-app \ No newline at end of file diff --git a/3.9/test/django-test-app b/3.9/test/django-test-app new file mode 120000 index 00000000..52cebf3b --- /dev/null +++ b/3.9/test/django-test-app @@ -0,0 +1 @@ +../../examples/django-test-app \ No newline at end of file diff --git a/3.9/test/from-dockerfile/Dockerfile.tpl b/3.9/test/from-dockerfile/Dockerfile.tpl new file mode 120000 index 00000000..ba88ccc4 --- /dev/null +++ b/3.9/test/from-dockerfile/Dockerfile.tpl @@ -0,0 +1 @@ +../../../src/test/from-dockerfile/Dockerfile.tpl \ No newline at end of file diff --git a/3.9/test/from-dockerfile/Dockerfile_no_s2i.tpl b/3.9/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/3.9/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/3.9/test/gunicorn-config-different-port-test-app b/3.9/test/gunicorn-config-different-port-test-app new file mode 120000 index 00000000..c8eb6171 --- /dev/null +++ b/3.9/test/gunicorn-config-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-config-different-port-test-app \ No newline at end of file diff --git a/3.9/test/gunicorn-different-port-test-app b/3.9/test/gunicorn-different-port-test-app new file mode 120000 index 00000000..72c8e433 --- /dev/null +++ b/3.9/test/gunicorn-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-different-port-test-app \ No newline at end of file diff --git a/3.9/test/gunicorn-python-configfile-different-port-test-app b/3.9/test/gunicorn-python-configfile-different-port-test-app new file mode 120000 index 00000000..9139ee17 --- /dev/null +++ b/3.9/test/gunicorn-python-configfile-different-port-test-app @@ -0,0 +1 @@ +../../examples/gunicorn-python-configfile-different-port-test-app \ No newline at end of file diff --git a/3.9/test/imagestreams b/3.9/test/imagestreams new file mode 120000 index 00000000..7a0aee9c --- /dev/null +++ b/3.9/test/imagestreams @@ -0,0 +1 @@ +../../imagestreams \ No newline at end of file diff --git a/3.9/test/locale-test-app b/3.9/test/locale-test-app new file mode 120000 index 00000000..9105ec08 --- /dev/null +++ b/3.9/test/locale-test-app @@ -0,0 +1 @@ +../../examples/locale-test-app \ No newline at end of file diff --git a/3.9/test/micropipenv-requirements-test-app b/3.9/test/micropipenv-requirements-test-app new file mode 120000 index 00000000..cb2ddcf0 --- /dev/null +++ b/3.9/test/micropipenv-requirements-test-app @@ -0,0 +1 @@ +../../examples/micropipenv-requirements-test-app \ No newline at end of file diff --git a/3.9/test/micropipenv-test-app/.gitignore b/3.9/test/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.9/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/3.9/test/micropipenv-test-app/.s2i/environment b/3.9/test/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/3.9/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/3.9/test/micropipenv-test-app/Pipfile b/3.9/test/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..117b223d --- /dev/null +++ b/3.9/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/3.9/test/micropipenv-test-app/Pipfile.lock b/3.9/test/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..25741732 --- /dev/null +++ b/3.9/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/3.9/test/micropipenv-test-app/setup.py b/3.9/test/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.9/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/3.9/test/micropipenv-test-app/testapp.py b/3.9/test/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.9/test/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.9/test/mod-wsgi-test-app b/3.9/test/mod-wsgi-test-app new file mode 120000 index 00000000..9d8c3338 --- /dev/null +++ b/3.9/test/mod-wsgi-test-app @@ -0,0 +1 @@ +../../examples/mod-wsgi-test-app \ No newline at end of file diff --git a/3.9/test/npm-virtualenv-uwsgi-test-app b/3.9/test/npm-virtualenv-uwsgi-test-app new file mode 120000 index 00000000..b34ae21e --- /dev/null +++ b/3.9/test/npm-virtualenv-uwsgi-test-app @@ -0,0 +1 @@ +../../examples/npm-virtualenv-uwsgi-test-app \ No newline at end of file diff --git a/3.9/test/numpy-test-app b/3.9/test/numpy-test-app new file mode 120000 index 00000000..b064f87f --- /dev/null +++ b/3.9/test/numpy-test-app @@ -0,0 +1 @@ +../../examples/numpy-test-app \ No newline at end of file diff --git a/3.9/test/pin-pipenv-version-test-app/.s2i/environment b/3.9/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..9bfe6709 --- /dev/null +++ b/3.9/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2021.5.29 diff --git a/3.9/test/pin-pipenv-version-test-app/app.sh b/3.9/test/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/3.9/test/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.9/test/pin-pipenv-version-test-app/requirements.txt b/3.9/test/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/3.9/test/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/3.9/test/pin-pipenv-version-test-app/wsgi.py b/3.9/test/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.9/test/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.9/test/pipenv-and-micropipenv-should-fail-test-app b/3.9/test/pipenv-and-micropipenv-should-fail-test-app new file mode 120000 index 00000000..cf830645 --- /dev/null +++ b/3.9/test/pipenv-and-micropipenv-should-fail-test-app @@ -0,0 +1 @@ +../../src/test/pipenv-and-micropipenv-should-fail-test-app \ No newline at end of file diff --git a/3.9/test/pipenv-test-app/.gitignore b/3.9/test/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/3.9/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/3.9/test/pipenv-test-app/.s2i/environment b/3.9/test/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/3.9/test/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/3.9/test/pipenv-test-app/Pipfile b/3.9/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..117b223d --- /dev/null +++ b/3.9/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/3.9/test/pipenv-test-app/Pipfile.lock b/3.9/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..25741732 --- /dev/null +++ b/3.9/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/3.9/test/pipenv-test-app/setup.py b/3.9/test/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/3.9/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/3.9/test/pipenv-test-app/testapp.py b/3.9/test/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/3.9/test/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/3.9/test/poetry-src-layout-test-app b/3.9/test/poetry-src-layout-test-app new file mode 120000 index 00000000..92d2990d --- /dev/null +++ b/3.9/test/poetry-src-layout-test-app @@ -0,0 +1 @@ +../../examples/poetry-src-layout-test-app \ No newline at end of file diff --git a/3.9/test/pyuwsgi-pipenv-test-app/.s2i/environment b/3.9/test/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..9bfe6709 --- /dev/null +++ b/3.9/test/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2021.5.29 diff --git a/3.9/test/pyuwsgi-pipenv-test-app/app.sh b/3.9/test/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/3.9/test/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/3.9/test/pyuwsgi-pipenv-test-app/requirements.txt b/3.9/test/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/3.9/test/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/3.9/test/pyuwsgi-pipenv-test-app/wsgi.py b/3.9/test/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/3.9/test/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/3.9/test/run b/3.9/test/run new file mode 100755 index 00000000..134639eb --- /dev/null +++ b/3.9/test/run @@ -0,0 +1,312 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + + +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/3.9/test/run-openshift-pytest b/3.9/test/run-openshift-pytest new file mode 120000 index 00000000..5063ae30 --- /dev/null +++ b/3.9/test/run-openshift-pytest @@ -0,0 +1 @@ +../../test/run-openshift-pytest \ No newline at end of file diff --git a/3.9/test/run-pytest b/3.9/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/3.9/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/3.9/test/setup-cfg-test-app b/3.9/test/setup-cfg-test-app new file mode 120000 index 00000000..71a2b0cb --- /dev/null +++ b/3.9/test/setup-cfg-test-app @@ -0,0 +1 @@ +../../examples/setup-cfg-test-app \ No newline at end of file diff --git a/3.9/test/setup-requirements-test-app b/3.9/test/setup-requirements-test-app new file mode 120000 index 00000000..c909ccb9 --- /dev/null +++ b/3.9/test/setup-requirements-test-app @@ -0,0 +1 @@ +../../examples/setup-requirements-test-app \ No newline at end of file diff --git a/3.9/test/setup-test-app b/3.9/test/setup-test-app new file mode 120000 index 00000000..1b49f26f --- /dev/null +++ b/3.9/test/setup-test-app @@ -0,0 +1 @@ +../../examples/setup-test-app \ No newline at end of file diff --git a/3.9/test/standalone-custom-pypi-index-test-app b/3.9/test/standalone-custom-pypi-index-test-app new file mode 120000 index 00000000..12941a9a --- /dev/null +++ b/3.9/test/standalone-custom-pypi-index-test-app @@ -0,0 +1 @@ +../../examples/standalone-custom-pypi-index-test-app \ No newline at end of file diff --git a/3.9/test/standalone-test-app b/3.9/test/standalone-test-app new file mode 120000 index 00000000..3f158948 --- /dev/null +++ b/3.9/test/standalone-test-app @@ -0,0 +1 @@ +../../examples/standalone-test-app \ No newline at end of file diff --git a/3.9/test/test-lib.sh b/3.9/test/test-lib.sh new file mode 120000 index 00000000..1ac99b93 --- /dev/null +++ b/3.9/test/test-lib.sh @@ -0,0 +1 @@ +../../common/test-lib.sh \ No newline at end of file diff --git a/3.9/test/test_container_application.py b/3.9/test/test_container_application.py new file mode 120000 index 00000000..25c1c1ec --- /dev/null +++ b/3.9/test/test_container_application.py @@ -0,0 +1 @@ +../../test/test_container_application.py \ No newline at end of file diff --git a/3.9/test/test_container_basics.py b/3.9/test/test_container_basics.py new file mode 120000 index 00000000..b85dc139 --- /dev/null +++ b/3.9/test/test_container_basics.py @@ -0,0 +1 @@ +../../test/test_container_basics.py \ No newline at end of file diff --git a/3.9/test/test_ocp_deploy_templates.py b/3.9/test/test_ocp_deploy_templates.py new file mode 120000 index 00000000..0b821390 --- /dev/null +++ b/3.9/test/test_ocp_deploy_templates.py @@ -0,0 +1 @@ +../../test/test_ocp_deploy_templates.py \ No newline at end of file diff --git a/3.9/test/test_ocp_helm_python_django_app.py b/3.9/test/test_ocp_helm_python_django_app.py new file mode 120000 index 00000000..34b4f0dd --- /dev/null +++ b/3.9/test/test_ocp_helm_python_django_app.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_app.py \ No newline at end of file diff --git a/3.9/test/test_ocp_helm_python_django_psql_persistent.py b/3.9/test/test_ocp_helm_python_django_psql_persistent.py new file mode 120000 index 00000000..64d2105d --- /dev/null +++ b/3.9/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_django_psql_persistent.py \ No newline at end of file diff --git a/3.9/test/test_ocp_helm_python_imagestreams.py b/3.9/test/test_ocp_helm_python_imagestreams.py new file mode 120000 index 00000000..0022607f --- /dev/null +++ b/3.9/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1 @@ +../../test/test_ocp_helm_python_imagestreams.py \ No newline at end of file diff --git a/3.9/test/test_ocp_imagestreams_quickstart.py b/3.9/test/test_ocp_imagestreams_quickstart.py new file mode 120000 index 00000000..18f8d7e2 --- /dev/null +++ b/3.9/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1 @@ +../../test/test_ocp_imagestreams_quickstart.py \ No newline at end of file diff --git a/3.9/test/test_ocp_python_ex_standalone.py b/3.9/test/test_ocp_python_ex_standalone.py new file mode 120000 index 00000000..bb6022f7 --- /dev/null +++ b/3.9/test/test_ocp_python_ex_standalone.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_standalone.py \ No newline at end of file diff --git a/3.9/test/test_ocp_python_ex_template.py b/3.9/test/test_ocp_python_ex_template.py new file mode 120000 index 00000000..b75080ca --- /dev/null +++ b/3.9/test/test_ocp_python_ex_template.py @@ -0,0 +1 @@ +../../test/test_ocp_python_ex_template.py \ No newline at end of file diff --git a/Makefile b/Makefile index a4b50be5..e366bf6a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,11 @@ -# Variables are documented in hack/build.sh. +# Variables are documented in common/build.sh. BASE_IMAGE_NAME = python -VERSIONS = 2.7 3.3 3.4 -OPENSHIFT_NAMESPACES = 3.3 +VERSIONS = 3.6 3.9 3.9-minimal 3.12 3.12-minimal 3.13 3.13-minimal 3.14 3.14-minimal +OPENSHIFT_NAMESPACES = +DOCKER_BUILD_CONTEXT = .. -# Include common Makefile code. -include hack/common.mk +# HACK: Ensure that 'git pull' for old clones doesn't cause confusion. +# New clones should use '--recursive'. +.PHONY: $(shell test -f common/common.mk || echo >&2 'Please do "git submodule update --init" first.') + +include common/common.mk diff --git a/README.md b/README.md index b396c928..42c46e6a 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,149 @@ -Python Docker images -==================== +Python container images +======================= +[![Build and push container images to Quay.io registry](https://github.com/sclorg/s2i-python-container/actions/workflows/build-and-push.yml/badge.svg)](https://github.com/sclorg/s2i-python-container/actions/workflows/build-and-push.yml) This repository contains the source for building various versions of -the Python application as a reproducible Docker image using +the Python application as a reproducible container image using [source-to-image](https://github.com/openshift/source-to-image). -Users can choose between RHEL and CentOS based builder images. -The resulting image can be run using [Docker](http://docker.io). +Users can choose between RHEL, Fedora and CentOS Stream based builder images. +The resulting image can be run using [podman](https://github.com/containers/libpod) or +[docker](http://docker.io). For more information about using these images with OpenShift, please see the -official [OpenShift Documentation](https://docs.openshift.org/latest/using_images/s2i_images/python.html). +official [OpenShift Documentation](https://docs.okd.io/latest/openshift_images/using_images/using-s21-images.html). -Versions ---------------- -Python versions currently provided are: -* python-2.7 -* python-3.3 -* python-3.4 - -RHEL versions currently supported are: -* RHEL7 +For more information about concepts used in these container images, see the +[Landing page](https://github.com/sclorg/welcome). -CentOS versions currently supported are: -* CentOS7 +Note: while the examples in this README are calling `podman`, you can replace any such calls by `docker` with the same arguments - -Installation +Contributing --------------- -To build a Python image, choose either the CentOS or RHEL based image: -* **RHEL based image** - - To build a RHEL based Python image, you need to run the build on a properly - subscribed RHEL machine. - - ``` - $ git clone https://github.com/openshift/sti-python.git - $ cd sti-python - $ make build TARGET=rhel7 VERSION=3.3 - ``` +In this repository [distgen](https://github.com/devexp-db/distgen/) > 1.0 is used for generating directories for Python versions. Also make sure distgen imports the jinja2 package >= 2.10. -* **CentOS based image** +Files in directories for a specific Python version are generated from templates in the src directory with values from specs/multispec.yml. - This image is available on DockerHub. To download it run: +A typical way how to contribute is: - ``` - $ docker pull openshift/python-33-centos7 - ``` +1. Add a feature or fix a bug in templates (src directory) or values (specs/multispec.yml file). +1. Commit the changes. +1. Regenerate all files via `make generate-all`. +1. Commit generated files. +1. Test changes via `make test TARGET=fedora VERSIONS=3.9` which will `build`, `tag` and `test` an image in one step. +1. Open a pull request! - To build a Python image from scratch run: +For more information about contributing, see +[the Contribution Guidelines](https://github.com/sclorg/welcome/blob/master/contribution.md). - ``` - $ git clone https://github.com/openshift/sti-python.git - $ cd sti-python - $ make build VERSION=3.3 - ``` - -**Notice: By omitting the `VERSION` parameter, the build/test action will be performed +Versions +--------------- +Currently supported versions are visible in the following table, expand an entry to see its container registry address. + +||CentOS Stream 9|CentOS Stream 10|Fedora|RHEL 8|RHEL 9|RHEL 10| +|:--|:--:|:--:|:--:|:--:|:--:|:--:| +|3.6||||
`registry.redhat.io/rhel8/python-36`
||| +|3.9|
`quay.io/sclorg/python-39-c9s`
||||
`registry.redhat.io/rhel9/python-39`
|| +|3.9-minimal|
`quay.io/sclorg/python-39-minimal-c9s`
|||||| +|3.12|
`quay.io/sclorg/python-312-c9s`
|
`quay.io/sclorg/python-312-c10s`
||
`registry.redhat.io/rhel8/python-312`
|
`registry.redhat.io/rhel9/python-312`
|| +|3.12-minimal|
`quay.io/sclorg/python-312-minimal-c9s`
|
`quay.io/sclorg/python-312-minimal-c10s`
||
`registry.redhat.io/rhel8/python-312-minimal`
|
`registry.redhat.io/rhel9/python-312-minimal`
|
`registry.redhat.io/rhel10/python-312-minimal`
| +|3.13||
`quay.io/sclorg/python-313-c10s`
|
`quay.io/fedora/python-313`
|||| +|3.13-minimal||
`quay.io/sclorg/python-313-minimal-c10s`
|
`quay.io/fedora/python-313-minimal`
|||| +|3.14|
`quay.io/sclorg/python-314-c9s`
||
`quay.io/fedora/python-314`
||
`registry.redhat.io/rhel9/python-314`
|| +|3.14-minimal|
`quay.io/sclorg/python-314-minimal-c9s`
|
`quay.io/sclorg/python-314-minimal-c10s`
|
`quay.io/fedora/python-314-minimal`
||
`registry.redhat.io/rhel9/python-314-minimal`
|
`registry.redhat.io/rhel10/python-314-minimal`
| + + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Download +-------- +To download one of the base Python images, follow the instructions you find in registries mentioned above. + +For example, CentOS Stream image can be downloaded via: + +``` +$ podman pull quay.io/c9s/python-39-c9s +``` + +Build +----- +To build a Python image from scratch run: + +``` +$ git clone https://github.com/sclorg/s2i-python-container.git +$ cd s2i-python-container +$ make build TARGET=c9s VERSIONS=3.9 +``` + +Where `TARGET` might be one of the supported platforms mentioned above. + +**Notice: By omitting the `VERSIONS` parameter, the build/test action will be performed on all provided versions of Python.** - Usage ---------------------------------- +----- +For information about usage of S2I Python images, see the documentation for each version in its folder. -For information about usage of Dockerfile for Python 2.7, -see [usage documentation](2.7/README.md). +Test +---- +This repository also provides a [S2I](https://github.com/openshift/source-to-image) test framework, +which launches tests to check functionality of simple Python applications built on top of the s2i-python-container image. -For information about usage of Dockerfile for Python 3.3, -see [usage documentation](3.3/README.md). +``` +$ cd s2i-python-container +$ make test TARGET=c9s VERSIONS=3.9 +``` -For information about usage of Dockerfile for Python 3.4, -see [usage documentation](3.4/README.md). +Where `TARGET` might be one of the supported platforms mentioned above. +**Notice: By omitting the `VERSIONS` parameter, the build/test action will be performed +on all provided versions of Python.** -Test ---------------------- -This repository also provides a [S2I](https://github.com/openshift/source-to-image) test framework, -which launches tests to check functionality of a simple Python application built on top of the sti-python image. -Users can choose between testing a Python test application based on a RHEL or CentOS image. +Repository organization +----------------------- +* **``** + + * **Dockerfile.c9s** -* **RHEL based image** + CentOS Stream based Dockerfile. - To test a RHEL7-based Python-3.3 image, you need to run the test on a properly subscribed RHEL machine. + * **Dockerfile.fedora** - ``` - $ cd sti-python - $ make test TARGET=rhel7 VERSION=3.3 - ``` + Fedora based Dockerfile. -* **CentOS based image** + * **Dockerfile.rhel8** & **Dockerfile.rhel9** - ``` - $ cd sti-python - $ make test VERSION=3.3 - ``` + RHEL 8/9 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. -**Notice: By omitting the `VERSION` parameter, the build/test action will be performed -on all provided versions of Python. Since we are currently providing only version `3.3` -you can omit this parameter.** + * **`s2i/bin/`** + This folder contains scripts that are run by [S2I](https://github.com/openshift/source-to-image): -Repository organization ------------------------- -* **``** + * **assemble** + + Used to install the sources into the location where the application + will be run and prepare the application for deployment (eg. installing + dependencies, etc.) - Dockerfile and scripts to build container images from. + * **run** -* **`hack/`** + This script is responsible for running the application by using the + application web server. - Folder containing scripts which are responsible for build and test actions performed by the `Makefile`. + * **usage*** + This script prints the usage of this image. -Image name structure ------------------------- -##### Structure: openshift/1-2-3 + * **`test/`** -1. Platform name (lowercase) - python -2. Platform version(without dots) - 33 -3. Base builder image - centos7/rhel7 + This folder contains a [S2I](https://github.com/openshift/source-to-image) + test framework with multiple test aplications testing different approaches. -Examples: `openshift/python-33-centos7`, `openshift/python-33-rhel7` + * **run** + Script that runs the [S2I](https://github.com/openshift/source-to-image) test framework. diff --git a/common b/common new file mode 160000 index 00000000..498153e3 --- /dev/null +++ b/common @@ -0,0 +1 @@ +Subproject commit 498153e34eaa412f7bbb36ed134767e681c00ceb diff --git a/examples/app-home-test-app/.s2i/environment b/examples/app-home-test-app/.s2i/environment new file mode 100644 index 00000000..45926fa1 --- /dev/null +++ b/examples/app-home-test-app/.s2i/environment @@ -0,0 +1 @@ +APP_HOME=project diff --git a/examples/app-home-test-app/project/wsgi.py b/examples/app-home-test-app/project/wsgi.py new file mode 100644 index 00000000..576a05b8 --- /dev/null +++ b/examples/app-home-test-app/project/wsgi.py @@ -0,0 +1,3 @@ +def application(environ, start_response): + start_response('200 OK', [('Content-Type','text/plain')]) + return [b"Hello World from app-home WSGI application!"] diff --git a/examples/app-home-test-app/requirements.txt b/examples/app-home-test-app/requirements.txt new file mode 100644 index 00000000..268bd603 --- /dev/null +++ b/examples/app-home-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/examples/app-module-test-app/.gitignore b/examples/app-module-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/app-module-test-app/.s2i/environment b/examples/app-module-test-app/.s2i/environment new file mode 100644 index 00000000..ac798d75 --- /dev/null +++ b/examples/app-module-test-app/.s2i/environment @@ -0,0 +1 @@ +APP_MODULE=app:application diff --git a/2.7/test/setup-test-app/testapp.py b/examples/app-module-test-app/app.py similarity index 100% rename from 2.7/test/setup-test-app/testapp.py rename to examples/app-module-test-app/app.py diff --git a/examples/app-module-test-app/requirements.txt b/examples/app-module-test-app/requirements.txt new file mode 100644 index 00000000..268bd603 --- /dev/null +++ b/examples/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/2.7/test/django-test-app/.gitignore b/examples/django-different-port-test-app/.gitignore similarity index 100% rename from 2.7/test/django-test-app/.gitignore rename to examples/django-different-port-test-app/.gitignore diff --git a/examples/django-different-port-test-app/.s2i/environment b/examples/django-different-port-test-app/.s2i/environment new file mode 100644 index 00000000..990affad --- /dev/null +++ b/examples/django-different-port-test-app/.s2i/environment @@ -0,0 +1 @@ +PORT=8085 diff --git a/2.7/test/django-test-app/manage.py b/examples/django-different-port-test-app/manage.py similarity index 100% rename from 2.7/test/django-test-app/manage.py rename to examples/django-different-port-test-app/manage.py diff --git a/2.7/test/django-test-app/project/__init__.py b/examples/django-different-port-test-app/project/__init__.py similarity index 100% rename from 2.7/test/django-test-app/project/__init__.py rename to examples/django-different-port-test-app/project/__init__.py diff --git a/3.3/test/django-test-app/project/settings.py b/examples/django-different-port-test-app/project/settings.py similarity index 65% rename from 3.3/test/django-test-app/project/settings.py rename to examples/django-different-port-test-app/project/settings.py index 3b5224b0..79072d09 100644 --- a/3.3/test/django-test-app/project/settings.py +++ b/examples/django-different-port-test-app/project/settings.py @@ -10,6 +10,7 @@ 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 @@ # 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 @@ '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/3.4/test/django-test-app/project/urls.py b/examples/django-different-port-test-app/project/urls.py similarity index 70% rename from 3.4/test/django-test-app/project/urls.py rename to examples/django-different-port-test-app/project/urls.py index b5e472c9..dc5093d8 100644 --- a/3.4/test/django-test-app/project/urls.py +++ b/examples/django-different-port-test-app/project/urls.py @@ -13,9 +13,15 @@ 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 +# django.conf.urls.url() has been deprecated in Django 3.0 and removed from Django 4.0+ +import django.conf.urls +if hasattr(django.conf.urls, 'url'): + from django.conf.urls import url +else: + from django.urls import re_path as url + urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), ] diff --git a/2.7/test/django-test-app/project/wsgi.py b/examples/django-different-port-test-app/project/wsgi.py similarity index 100% rename from 2.7/test/django-test-app/project/wsgi.py rename to examples/django-different-port-test-app/project/wsgi.py diff --git a/examples/django-different-port-test-app/requirements.txt b/examples/django-different-port-test-app/requirements.txt new file mode 100644 index 00000000..d5de07e5 --- /dev/null +++ b/examples/django-different-port-test-app/requirements.txt @@ -0,0 +1,3 @@ +Django>=3,<5; python_version >='3.12' +Django>=2,<3; python_version >='3.9' and python_version <'3.12' +Django<2; python_version <'3.9' \ No newline at end of file diff --git a/examples/django-postgresql-persistent.json b/examples/django-postgresql-persistent.json new file mode 100644 index 00000000..1440cd1b --- /dev/null +++ b/examples/django-postgresql-persistent.json @@ -0,0 +1,511 @@ +{ + "kind": "Template", + "apiVersion": "template.openshift.io/v1", + "metadata": { + "name": "django-psql-persistent", + "annotations": { + "openshift.io/display-name": "Django + PostgreSQL (Persistent)", + "description": "An example Django application with a PostgreSQL database. For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.", + "tags": "quickstart,python,django", + "iconClass": "icon-python", + "template.openshift.io/long-description": "This template defines resources needed to develop a Django based application, including a build configuration, application deployment configuration, and database deployment configuration.", + "template.openshift.io/provider-display-name": "Red Hat, Inc.", + "template.openshift.io/documentation-url": "https://github.com/sclorg/django-ex", + "template.openshift.io/support-url": "https://access.redhat.com" + } + }, + "message": "The following service(s) have been created in your project: ${NAME}, ${DATABASE_SERVICE_NAME}.\n\nFor more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.", + "labels": { + "template": "django-psql-persistent" + }, + "objects": [ + { + "kind": "Secret", + "apiVersion": "v1", + "metadata": { + "name": "${NAME}" + }, + "stringData" : { + "database-user" : "${DATABASE_USER}", + "database-password" : "${DATABASE_PASSWORD}", + "django-secret-key" : "${DJANGO_SECRET_KEY}" + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Exposes and load balances the application pods", + "service.alpha.openshift.io/dependencies": "[{\"name\": \"${DATABASE_SERVICE_NAME}\", \"kind\": \"Service\"}]" + } + }, + "spec": { + "ports": [ + { + "name": "web", + "port": 8080, + "targetPort": 8080 + } + ], + "selector": { + "name": "${NAME}" + } + } + }, + { + "kind": "Route", + "apiVersion": "route.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "template.openshift.io/expose-uri": "http://{.spec.host}{.spec.path}" + } + }, + "spec": { + "host": "${APPLICATION_DOMAIN}", + "to": { + "kind": "Service", + "name": "${NAME}" + } + } + }, + { + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Keeps track of changes in the application image" + } + } + }, + { + "kind": "BuildConfig", + "apiVersion": "build.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Defines how to build the application", + "template.alpha.openshift.io/wait-for-ready": "true" + } + }, + "spec": { + "source": { + "type": "Git", + "git": { + "uri": "${SOURCE_REPOSITORY_URL}", + "ref": "${SOURCE_REPOSITORY_REF}" + }, + "contextDir": "${CONTEXT_DIR}" + }, + "strategy": { + "type": "Source", + "sourceStrategy": { + "from": { + "kind": "ImageStreamTag", + "namespace": "${NAMESPACE}", + "name": "python:3.5" + }, + "env": [ + { + "name": "PIP_INDEX_URL", + "value": "${PIP_INDEX_URL}" + } + ] + } + }, + "output": { + "to": { + "kind": "ImageStreamTag", + "name": "${NAME}:latest" + } + }, + "triggers": [ + { + "type": "ImageChange" + }, + { + "type": "ConfigChange" + }, + { + "type": "GitHub", + "github": { + "secret": "${GITHUB_WEBHOOK_SECRET}" + } + } + ], + "postCommit": { + "script": "./manage.py test" + } + } + }, + { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Defines how to deploy the application server", + "template.alpha.openshift.io/wait-for-ready": "true", + "image.openshift.io/triggers": "[{\"from\":{\"kind\":\"ImageStreamTag\",\"name\":\"${NAME}:latest\"},\"fieldPath\": \"spec.template.spec.containers[0].image\"}]" + } + }, + "spec": { + "strategy": { + "type": "Recreate" + }, + "replicas": 1, + "selector": { + "matchLabels": { + "name": "${NAME}" + } + }, + "template": { + "metadata": { + "name": "${NAME}", + "labels": { + "name": "${NAME}" + } + }, + "spec": { + "containers": [ + { + "name": "django-psql-persistent", + "image": " ", + "ports": [ + { + "containerPort": 8080 + } + ], + "readinessProbe": { + "timeoutSeconds": 3, + "initialDelaySeconds": 3, + "httpGet": { + "path": "/health", + "port": 8080 + } + }, + "livenessProbe": { + "timeoutSeconds": 3, + "initialDelaySeconds": 30, + "httpGet": { + "path": "/health", + "port": 8080 + } + }, + "env": [ + { + "name": "DATABASE_SERVICE_NAME", + "value": "${DATABASE_SERVICE_NAME}" + }, + { + "name": "DATABASE_ENGINE", + "value": "${DATABASE_ENGINE}" + }, + { + "name": "DATABASE_NAME", + "value": "${DATABASE_NAME}" + }, + { + "name": "DATABASE_USER", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-user" + } + } + }, + { + "name": "DATABASE_PASSWORD", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-password" + } + } + }, + { + "name": "APP_CONFIG", + "value": "${APP_CONFIG}" + }, + { + "name": "DJANGO_SECRET_KEY", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "django-secret-key" + } + } + } + ], + "resources": { + "limits": { + "memory": "${MEMORY_LIMIT}" + } + } + } + ] + } + } + } + }, + { + "kind": "PersistentVolumeClaim", + "apiVersion": "v1", + "metadata": { + "name": "${DATABASE_SERVICE_NAME}" + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "resources": { + "requests": { + "storage": "${VOLUME_CAPACITY}" + } + } + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "annotations": { + "description": "Exposes the database server" + } + }, + "spec": { + "ports": [ + { + "name": "postgresql", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "name": "${DATABASE_SERVICE_NAME}" + } + } + }, + { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "annotations": { + "description": "Defines how to deploy the database", + "template.alpha.openshift.io/wait-for-ready": "true", + "image.openshift.io/triggers": "[{\"from\":{\"kind\":\"ImageStreamTag\",\"name\":\"postgresql:12\"},\"fieldPath\": \"spec.template.spec.containers[0].image\"}]" + } + }, + "spec": { + "strategy": { + "type": "Recreate" + }, + "replicas": 1, + "selector": { + "matchLabels":{ + "name": "${DATABASE_SERVICE_NAME}" + } + }, + "template": { + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "labels": { + "name": "${DATABASE_SERVICE_NAME}" + } + }, + "spec": { + "volumes": [ + { + "name": "${DATABASE_SERVICE_NAME}-data", + "persistentVolumeClaim": { + "claimName": "${DATABASE_SERVICE_NAME}" + } + } + ], + "containers": [ + { + "name": "postgresql", + "image": " ", + "ports": [ + { + "containerPort": 5432 + } + ], + "env": [ + { + "name": "POSTGRESQL_USER", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-user" + } + } + }, + { + "name": "POSTGRESQL_PASSWORD", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-password" + } + } + }, + { + "name": "POSTGRESQL_DATABASE", + "value": "${DATABASE_NAME}" + } + ], + "volumeMounts": [ + { + "name": "${DATABASE_SERVICE_NAME}-data", + "mountPath": "/var/lib/pgsql/data" + } + ], + "readinessProbe": { + "timeoutSeconds": 1, + "initialDelaySeconds": 5, + "exec": { + "command": [ "/bin/sh", "-i", "-c", "psql -h 127.0.0.1 -U ${POSTGRESQL_USER} -q -d ${POSTGRESQL_DATABASE} -c 'SELECT 1'"] + } + }, + "livenessProbe": { + "timeoutSeconds": 1, + "initialDelaySeconds": 30, + "tcpSocket": { + "port": 5432 + } + }, + "resources": { + "limits": { + "memory": "${MEMORY_POSTGRESQL_LIMIT}" + } + } + } + ] + } + } + } + } + ], + "parameters": [ + { + "name": "NAME", + "displayName": "Name", + "description": "The name assigned to all of the frontend objects defined in this template.", + "required": true, + "value": "django-psql-persistent" + }, + { + "name": "NAMESPACE", + "displayName": "Namespace", + "required": true, + "description": "The OpenShift Namespace where the ImageStream resides.", + "value": "openshift" + }, + { + "name": "MEMORY_LIMIT", + "displayName": "Memory Limit", + "required": true, + "description": "Maximum amount of memory the Django container can use.", + "value": "512Mi" + }, + { + "name": "MEMORY_POSTGRESQL_LIMIT", + "displayName": "Memory Limit (PostgreSQL)", + "required": true, + "description": "Maximum amount of memory the PostgreSQL container can use.", + "value": "512Mi" + }, + { + "name": "VOLUME_CAPACITY", + "displayName": "Volume Capacity", + "description": "Volume space available for data, e.g. 512Mi, 2Gi", + "value": "1Gi", + "required": true + }, + { + "name": "SOURCE_REPOSITORY_URL", + "displayName": "Git Repository URL", + "required": true, + "description": "The URL of the repository with your application source code.", + "value": "https://github.com/sclorg/django-ex.git" + }, + { + "name": "SOURCE_REPOSITORY_REF", + "displayName": "Git Reference", + "description": "Set this to a branch name, tag or other ref of your repository if you are not using the default branch." + }, + { + "name": "CONTEXT_DIR", + "displayName": "Context Directory", + "description": "Set this to the relative path to your project if it is not in the root of your repository." + }, + { + "name": "APPLICATION_DOMAIN", + "displayName": "Application Hostname", + "description": "The exposed hostname that will route to the Django service, if left blank a value will be defaulted.", + "value": "" + }, + { + "name": "GITHUB_WEBHOOK_SECRET", + "displayName": "GitHub Webhook Secret", + "description": "Github trigger secret. A difficult to guess string encoded as part of the webhook URL. Not encrypted.", + "generate": "expression", + "from": "[a-zA-Z0-9]{40}" + }, + { + "name": "DATABASE_SERVICE_NAME", + "displayName": "Database Service Name", + "required": true, + "value": "postgresql" + }, + { + "name": "DATABASE_ENGINE", + "displayName": "Database Engine", + "required": true, + "description": "Database engine: postgresql, mysql or sqlite (default).", + "value": "postgresql" + }, + { + "name": "DATABASE_NAME", + "displayName": "Database Name", + "required": true, + "value": "default" + }, + { + "name": "DATABASE_USER", + "displayName": "Database Username", + "required": true, + "value": "django" + }, + { + "name": "DATABASE_PASSWORD", + "displayName": "Database User Password", + "generate": "expression", + "from": "[a-zA-Z0-9]{16}" + }, + { + "name": "APP_CONFIG", + "displayName": "Application Configuration File Path", + "description": "Relative path to Gunicorn configuration file (optional)." + }, + { + "name": "DJANGO_SECRET_KEY", + "displayName": "Django Secret Key", + "description": "Set this to a long random string.", + "generate": "expression", + "from": "[\\w]{50}" + }, + { + "name": "PIP_INDEX_URL", + "displayName": "Custom PyPi Index URL", + "description": "The custom PyPi index URL", + "value": "" + } + ] +} diff --git a/examples/django-postgresql.json b/examples/django-postgresql.json new file mode 100644 index 00000000..9b50e715 --- /dev/null +++ b/examples/django-postgresql.json @@ -0,0 +1,486 @@ +{ + "kind": "Template", + "apiVersion": "template.openshift.io/v1", + "metadata": { + "name": "django-psql-example", + "annotations": { + "openshift.io/display-name": "Django + PostgreSQL (Ephemeral)", + "description": "An example Django application with a PostgreSQL database. For more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.\n\nWARNING: Any data stored will be lost upon pod destruction. Only use this template for testing.", + "tags": "quickstart,python,django", + "iconClass": "icon-python", + "template.openshift.io/long-description": "This template defines resources needed to develop a Django based application, including a build configuration, application deployment configuration, and database deployment configuration. The database is stored in non-persistent storage, so this configuration should be used for experimental purposes only.", + "template.openshift.io/provider-display-name": "Red Hat, Inc.", + "template.openshift.io/documentation-url": "https://github.com/sclorg/django-ex", + "template.openshift.io/support-url": "https://access.redhat.com" + } + }, + "message": "The following service(s) have been created in your project: ${NAME}, ${DATABASE_SERVICE_NAME}.\n\nFor more information about using this template, including OpenShift considerations, see https://github.com/sclorg/django-ex/blob/master/README.md.", + "labels": { + "template": "django-psql-example" + }, + "objects": [ + { + "kind": "Secret", + "apiVersion": "v1", + "metadata": { + "name": "${NAME}" + }, + "stringData" : { + "database-user" : "${DATABASE_USER}", + "database-password" : "${DATABASE_PASSWORD}", + "django-secret-key" : "${DJANGO_SECRET_KEY}" + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Exposes and load balances the application pods", + "service.alpha.openshift.io/dependencies": "[{\"name\": \"${DATABASE_SERVICE_NAME}\", \"kind\": \"Service\"}]" + } + }, + "spec": { + "ports": [ + { + "name": "web", + "port": 8080, + "targetPort": 8080 + } + ], + "selector": { + "name": "${NAME}" + } + } + }, + { + "kind": "Route", + "apiVersion": "route.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "template.openshift.io/expose-uri": "http://{.spec.host}{.spec.path}" + } + }, + "spec": { + "host": "${APPLICATION_DOMAIN}", + "to": { + "kind": "Service", + "name": "${NAME}" + } + } + }, + { + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Keeps track of changes in the application image" + } + } + }, + { + "kind": "BuildConfig", + "apiVersion": "build.openshift.io/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Defines how to build the application", + "template.alpha.openshift.io/wait-for-ready": "true" + } + }, + "spec": { + "source": { + "type": "Git", + "git": { + "uri": "${SOURCE_REPOSITORY_URL}", + "ref": "${SOURCE_REPOSITORY_REF}" + }, + "contextDir": "${CONTEXT_DIR}" + }, + "strategy": { + "type": "Source", + "sourceStrategy": { + "from": { + "kind": "ImageStreamTag", + "namespace": "${NAMESPACE}", + "name": "python:3.5" + }, + "env": [ + { + "name": "PIP_INDEX_URL", + "value": "${PIP_INDEX_URL}" + } + ] + } + }, + "output": { + "to": { + "kind": "ImageStreamTag", + "name": "${NAME}:latest" + } + }, + "triggers": [ + { + "type": "ImageChange" + }, + { + "type": "ConfigChange" + }, + { + "type": "GitHub", + "github": { + "secret": "${GITHUB_WEBHOOK_SECRET}" + } + } + ], + "postCommit": { + "script": "./manage.py test" + } + } + }, + { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "${NAME}", + "annotations": { + "description": "Defines how to deploy the application server", + "template.alpha.openshift.io/wait-for-ready": "true", + "image.openshift.io/triggers": "[{\"from\":{\"kind\":\"ImageStreamTag\",\"name\":\"${NAME}:latest\"},\"fieldPath\": \"spec.template.spec.containers[0].image\"}]" + + } + }, + "spec": { + "strategy": { + "type": "Recreate" + }, + "replicas": 1, + "selector": { + "matchLabels": { + "name": "${NAME}" + } + }, + "template": { + "metadata": { + "name": "${NAME}", + "labels": { + "name": "${NAME}" + } + }, + "spec": { + "containers": [ + { + "name": "django-psql-example", + "image": " ", + "ports": [ + { + "containerPort": 8080 + } + ], + "readinessProbe": { + "timeoutSeconds": 3, + "initialDelaySeconds": 3, + "httpGet": { + "path": "/health", + "port": 8080 + } + }, + "livenessProbe": { + "timeoutSeconds": 3, + "initialDelaySeconds": 30, + "httpGet": { + "path": "/health", + "port": 8080 + } + }, + "env": [ + { + "name": "DATABASE_SERVICE_NAME", + "value": "${DATABASE_SERVICE_NAME}" + }, + { + "name": "DATABASE_ENGINE", + "value": "${DATABASE_ENGINE}" + }, + { + "name": "DATABASE_NAME", + "value": "${DATABASE_NAME}" + }, + { + "name": "DATABASE_USER", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-user" + } + } + }, + { + "name": "DATABASE_PASSWORD", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-password" + } + } + }, + { + "name": "APP_CONFIG", + "value": "${APP_CONFIG}" + }, + { + "name": "DJANGO_SECRET_KEY", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "django-secret-key" + } + } + } + ], + "resources": { + "limits": { + "memory": "${MEMORY_LIMIT}" + } + } + } + ] + } + } + } + }, + { + "kind": "Service", + "apiVersion": "v1", + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "annotations": { + "description": "Exposes the database server" + } + }, + "spec": { + "ports": [ + { + "name": "postgresql", + "port": 5432, + "targetPort": 5432 + } + ], + "selector": { + "name": "${DATABASE_SERVICE_NAME}" + } + } + }, + { + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "annotations": { + "description": "Defines how to deploy the database", + "template.alpha.openshift.io/wait-for-ready": "true", + "image.openshift.io/triggers": "[{\"from\":{\"kind\":\"ImageStreamTag\",\"name\":\"postgresql:12\"},\"fieldPath\": \"spec.template.spec.containers[0].image\"}]" + } + }, + "spec": { + "strategy": { + "type": "Recreate" + }, + "replicas": 1, + "selector": { + "matchLabels":{ + "name": "${DATABASE_SERVICE_NAME}" + } + }, + "template": { + "metadata": { + "name": "${DATABASE_SERVICE_NAME}", + "labels": { + "name": "${DATABASE_SERVICE_NAME}" + } + }, + "spec": { + "volumes": [ + { + "name": "data", + "emptyDir": {} + } + ], + "containers": [ + { + "name": "postgresql", + "image": " ", + "ports": [ + { + "containerPort": 5432 + } + ], + "env": [ + { + "name": "POSTGRESQL_USER", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-user" + } + } + }, + { + "name": "POSTGRESQL_PASSWORD", + "valueFrom": { + "secretKeyRef" : { + "name" : "${NAME}", + "key" : "database-password" + } + } + }, + { + "name": "POSTGRESQL_DATABASE", + "value": "${DATABASE_NAME}" + } + ], + "volumeMounts": [ + { + "name": "data", + "mountPath": "/var/lib/pgsql/data" + } + ], + "readinessProbe": { + "timeoutSeconds": 1, + "initialDelaySeconds": 5, + "exec": { + "command": [ "/bin/sh", "-i", "-c", "psql -h 127.0.0.1 -U ${POSTGRESQL_USER} -q -d ${POSTGRESQL_DATABASE} -c 'SELECT 1'"] + } + }, + "livenessProbe": { + "timeoutSeconds": 1, + "initialDelaySeconds": 30, + "tcpSocket": { + "port": 5432 + } + }, + "resources": { + "limits": { + "memory": "${MEMORY_POSTGRESQL_LIMIT}" + } + } + } + ] + } + } + } + } + ], + "parameters": [ + { + "name": "NAME", + "displayName": "Name", + "description": "The name assigned to all of the frontend objects defined in this template.", + "required": true, + "value": "django-psql-example" + }, + { + "name": "NAMESPACE", + "displayName": "Namespace", + "required": true, + "description": "The OpenShift Namespace where the ImageStream resides.", + "value": "openshift" + }, + { + "name": "MEMORY_LIMIT", + "displayName": "Memory Limit", + "required": true, + "description": "Maximum amount of memory the Django container can use.", + "value": "512Mi" + }, + { + "name": "MEMORY_POSTGRESQL_LIMIT", + "displayName": "Memory Limit (PostgreSQL)", + "required": true, + "description": "Maximum amount of memory the PostgreSQL container can use.", + "value": "512Mi" + }, + { + "name": "SOURCE_REPOSITORY_URL", + "displayName": "Git Repository URL", + "required": true, + "description": "The URL of the repository with your application source code.", + "value": "https://github.com/sclorg/django-ex.git" + }, + { + "name": "SOURCE_REPOSITORY_REF", + "displayName": "Git Reference", + "description": "Set this to a branch name, tag or other ref of your repository if you are not using the default branch." + }, + { + "name": "CONTEXT_DIR", + "displayName": "Context Directory", + "description": "Set this to the relative path to your project if it is not in the root of your repository." + }, + { + "name": "APPLICATION_DOMAIN", + "displayName": "Application Hostname", + "description": "The exposed hostname that will route to the Django service, if left blank a value will be defaulted.", + "value": "" + }, + { + "name": "GITHUB_WEBHOOK_SECRET", + "displayName": "GitHub Webhook Secret", + "description": "Github trigger secret. A difficult to guess string encoded as part of the webhook URL. Not encrypted.", + "generate": "expression", + "from": "[a-zA-Z0-9]{40}" + }, + { + "name": "DATABASE_SERVICE_NAME", + "displayName": "Database Service Name", + "required": true, + "value": "postgresql" + }, + { + "name": "DATABASE_ENGINE", + "displayName": "Database Engine", + "required": true, + "description": "Database engine: postgresql, mysql or sqlite (default).", + "value": "postgresql" + }, + { + "name": "DATABASE_NAME", + "displayName": "Database Name", + "required": true, + "value": "default" + }, + { + "name": "DATABASE_USER", + "displayName": "Database Username", + "required": true, + "value": "django" + }, + { + "name": "DATABASE_PASSWORD", + "displayName": "Database User Password", + "generate": "expression", + "from": "[a-zA-Z0-9]{16}" + }, + { + "name": "APP_CONFIG", + "displayName": "Application Configuration File Path", + "description": "Relative path to Gunicorn configuration file (optional)." + }, + { + "name": "DJANGO_SECRET_KEY", + "displayName": "Django Secret Key", + "description": "Set this to a long random string.", + "generate": "expression", + "from": "[\\w]{50}" + }, + { + "name": "PIP_INDEX_URL", + "displayName": "Custom PyPi Index URL", + "description": "The custom PyPi index URL", + "value": "" + } + ] +} diff --git a/3.3/test/django-test-app/.gitignore b/examples/django-test-app/.gitignore similarity index 100% rename from 3.3/test/django-test-app/.gitignore rename to examples/django-test-app/.gitignore diff --git a/3.3/test/django-test-app/manage.py b/examples/django-test-app/manage.py similarity index 100% rename from 3.3/test/django-test-app/manage.py rename to examples/django-test-app/manage.py diff --git a/3.3/test/django-test-app/project/__init__.py b/examples/django-test-app/project/__init__.py similarity index 100% rename from 3.3/test/django-test-app/project/__init__.py rename to examples/django-test-app/project/__init__.py diff --git a/2.7/test/django-test-app/project/settings.py b/examples/django-test-app/project/settings.py similarity index 65% rename from 2.7/test/django-test-app/project/settings.py rename to examples/django-test-app/project/settings.py index 3b5224b0..79072d09 100644 --- a/2.7/test/django-test-app/project/settings.py +++ b/examples/django-test-app/project/settings.py @@ -10,6 +10,7 @@ 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 @@ # 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 @@ '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/2.7/test/django-test-app/project/urls.py b/examples/django-test-app/project/urls.py similarity index 70% rename from 2.7/test/django-test-app/project/urls.py rename to examples/django-test-app/project/urls.py index b5e472c9..7c1d8e8c 100644 --- a/2.7/test/django-test-app/project/urls.py +++ b/examples/django-test-app/project/urls.py @@ -13,9 +13,16 @@ 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 +# django.conf.urls.url() has been deprecated in Django 3.0 and removed from Django 4.0+ +import django.conf.urls +if hasattr(django.conf.urls, 'url'): + from django.conf.urls import url +else: + from django.urls import re_path as url + + urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), + url(r'^admin/', admin.site.urls), ] diff --git a/3.3/test/django-test-app/project/wsgi.py b/examples/django-test-app/project/wsgi.py similarity index 100% rename from 3.3/test/django-test-app/project/wsgi.py rename to examples/django-test-app/project/wsgi.py diff --git a/examples/django-test-app/requirements.txt b/examples/django-test-app/requirements.txt new file mode 100644 index 00000000..6603dffb --- /dev/null +++ b/examples/django-test-app/requirements.txt @@ -0,0 +1,3 @@ +Django>=3,<5; python_version >='3.12' +Django>=2,<3; python_version >='3.9' and python_version <'3.12' +Django<2; python_version <'3.9' diff --git a/examples/gunicorn-config-different-port-test-app/.gitignore b/examples/gunicorn-config-different-port-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/gunicorn-config-different-port-test-app/.s2i/environment b/examples/gunicorn-config-different-port-test-app/.s2i/environment new file mode 100644 index 00000000..d975f74c --- /dev/null +++ b/examples/gunicorn-config-different-port-test-app/.s2i/environment @@ -0,0 +1,2 @@ +APP_MODULE=app:application +APP_CONFIG=./gunicorn.conf.py diff --git a/3.3/test/setup-test-app/testapp.py b/examples/gunicorn-config-different-port-test-app/app.py similarity index 100% rename from 3.3/test/setup-test-app/testapp.py rename to examples/gunicorn-config-different-port-test-app/app.py diff --git a/examples/gunicorn-config-different-port-test-app/gunicorn.conf.py b/examples/gunicorn-config-different-port-test-app/gunicorn.conf.py new file mode 100644 index 00000000..1ae04466 --- /dev/null +++ b/examples/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/examples/gunicorn-config-different-port-test-app/requirements.txt b/examples/gunicorn-config-different-port-test-app/requirements.txt new file mode 100644 index 00000000..268bd603 --- /dev/null +++ b/examples/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/examples/gunicorn-different-port-test-app/.gitignore b/examples/gunicorn-different-port-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/gunicorn-different-port-test-app/.s2i/environment b/examples/gunicorn-different-port-test-app/.s2i/environment new file mode 100644 index 00000000..164666b4 --- /dev/null +++ b/examples/gunicorn-different-port-test-app/.s2i/environment @@ -0,0 +1,2 @@ +APP_MODULE=app:application +PORT=8085 diff --git a/3.4/test/setup-test-app/testapp.py b/examples/gunicorn-different-port-test-app/app.py similarity index 100% rename from 3.4/test/setup-test-app/testapp.py rename to examples/gunicorn-different-port-test-app/app.py diff --git a/examples/gunicorn-different-port-test-app/requirements.txt b/examples/gunicorn-different-port-test-app/requirements.txt new file mode 100644 index 00000000..268bd603 --- /dev/null +++ b/examples/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/examples/gunicorn-python-configfile-different-port-test-app/.gitignore b/examples/gunicorn-python-configfile-different-port-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/gunicorn-python-configfile-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/examples/gunicorn-python-configfile-different-port-test-app/app.py b/examples/gunicorn-python-configfile-different-port-test-app/app.py new file mode 100644 index 00000000..09fee1b7 --- /dev/null +++ b/examples/gunicorn-python-configfile-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/examples/gunicorn-python-configfile-different-port-test-app/gunicorn.conf.py b/examples/gunicorn-python-configfile-different-port-test-app/gunicorn.conf.py new file mode 100644 index 00000000..bcb267f6 --- /dev/null +++ b/examples/gunicorn-python-configfile-different-port-test-app/gunicorn.conf.py @@ -0,0 +1,6 @@ +import multiprocessing + +bind = "0.0.0.0:8085" +workers = multiprocessing.cpu_count() * 2 + 1 + +wsgi_app = "app:application" diff --git a/examples/gunicorn-python-configfile-different-port-test-app/requirements.txt b/examples/gunicorn-python-configfile-different-port-test-app/requirements.txt new file mode 100644 index 00000000..8819996f --- /dev/null +++ b/examples/gunicorn-python-configfile-different-port-test-app/requirements.txt @@ -0,0 +1 @@ +gunicorn>=20.1.0 diff --git a/examples/locale-test-app/requirements.txt b/examples/locale-test-app/requirements.txt new file mode 100644 index 00000000..51bc227d --- /dev/null +++ b/examples/locale-test-app/requirements.txt @@ -0,0 +1,3 @@ +Flask +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' diff --git a/examples/locale-test-app/wsgi.py b/examples/locale-test-app/wsgi.py new file mode 100644 index 00000000..e14f14bc --- /dev/null +++ b/examples/locale-test-app/wsgi.py @@ -0,0 +1,41 @@ +import locale +import os +import sys + +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + assert os.environ['PYTHONIOENCODING'] == 'UTF-8' + assert os.environ['LC_ALL'] == 'en_US.UTF-8' + assert os.environ['LANG'] == 'en_US.UTF-8' + + assert locale.getdefaultlocale() == ('en_US', 'UTF-8') + assert locale.getpreferredencoding() == 'UTF-8' + + print(u'\u292e') + + return b'Hello World from locale test application!' + +print('-- GLOBAL --') + +for k,v in os.environ.items(): + print('%r=%r' % (k, v)) + +print() + +print(sys.stdout.encoding) +print(locale.getlocale()) +print(locale.getdefaultlocale()) +print(locale.getpreferredencoding()) + +try: + print(u'\u292e') +except Exception as e: + print(e) + +print('------------') + +if __name__ == '__main__': + application.run() diff --git a/examples/micropipenv-requirements-test-app/.s2i/environment b/examples/micropipenv-requirements-test-app/.s2i/environment new file mode 100644 index 00000000..2d40b156 --- /dev/null +++ b/examples/micropipenv-requirements-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_MICROPIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/examples/micropipenv-requirements-test-app/app.py b/examples/micropipenv-requirements-test-app/app.py new file mode 100644 index 00000000..bbac2641 --- /dev/null +++ b/examples/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/examples/micropipenv-requirements-test-app/requirements.txt b/examples/micropipenv-requirements-test-app/requirements.txt new file mode 100644 index 00000000..58035c4b --- /dev/null +++ b/examples/micropipenv-requirements-test-app/requirements.txt @@ -0,0 +1,6 @@ +mod_wsgi==5.0.1; python_version >= '3.12' +mod_wsgi<5.0.0; python_version < '3.12' +Flask +# Tests that gssapi package is installable. +# This test is ideal place because it's skipped for minimal containers. +gssapi \ No newline at end of file diff --git a/examples/micropipenv-requirements-test-app/wsgi.py b/examples/micropipenv-requirements-test-app/wsgi.py new file mode 100644 index 00000000..9e724694 --- /dev/null +++ b/examples/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/examples/micropipenv-test-app/.gitignore b/examples/micropipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/micropipenv-test-app/.s2i/environment b/examples/micropipenv-test-app/.s2i/environment new file mode 100644 index 00000000..29efed4a --- /dev/null +++ b/examples/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/examples/micropipenv-test-app/Pipfile b/examples/micropipenv-test-app/Pipfile new file mode 100644 index 00000000..fc4d0c80 --- /dev/null +++ b/examples/micropipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.13" diff --git a/examples/micropipenv-test-app/Pipfile.lock b/examples/micropipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..b0aa1027 --- /dev/null +++ b/examples/micropipenv-test-app/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==8.3.3" + } + } +} diff --git a/examples/micropipenv-test-app/setup.py b/examples/micropipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/examples/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/examples/micropipenv-test-app/testapp.py b/examples/micropipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/examples/micropipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/examples/mod-wsgi-test-app/app.py b/examples/mod-wsgi-test-app/app.py new file mode 100644 index 00000000..bce8b2dc --- /dev/null +++ b/examples/mod-wsgi-test-app/app.py @@ -0,0 +1,17 @@ +import os +try: + import mod_wsgi.express.cli as server_wrapper +except ImportError: + import mod_wsgi.server as server_wrapper + +server_wrapper.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/examples/mod-wsgi-test-app/requirements.txt b/examples/mod-wsgi-test-app/requirements.txt new file mode 100644 index 00000000..e74c313b --- /dev/null +++ b/examples/mod-wsgi-test-app/requirements.txt @@ -0,0 +1,3 @@ +mod_wsgi>=5.0.0; python_version >= '3.12' +mod_wsgi<5.0.0; python_version < '3.12' +Flask diff --git a/examples/mod-wsgi-test-app/wsgi.py b/examples/mod-wsgi-test-app/wsgi.py new file mode 100644 index 00000000..9e724694 --- /dev/null +++ b/examples/mod-wsgi-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/examples/npm-virtualenv-uwsgi-test-app/.s2i/environment b/examples/npm-virtualenv-uwsgi-test-app/.s2i/environment new file mode 100644 index 00000000..c1761606 --- /dev/null +++ b/examples/npm-virtualenv-uwsgi-test-app/.s2i/environment @@ -0,0 +1 @@ +UPGRADE_PIP_TO_LATEST=1 diff --git a/examples/npm-virtualenv-uwsgi-test-app/app.sh b/examples/npm-virtualenv-uwsgi-test-app/app.sh new file mode 100755 index 00000000..2272c397 --- /dev/null +++ b/examples/npm-virtualenv-uwsgi-test-app/app.sh @@ -0,0 +1,63 @@ +#!/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 $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." + echo "Output of the pip install command is:" + python -m pip install -U --no-deps $pkg + 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 "const { 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/examples/npm-virtualenv-uwsgi-test-app/requirements.txt b/examples/npm-virtualenv-uwsgi-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/examples/npm-virtualenv-uwsgi-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/examples/npm-virtualenv-uwsgi-test-app/wsgi.py b/examples/npm-virtualenv-uwsgi-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/examples/npm-virtualenv-uwsgi-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/examples/numpy-test-app/requirements.txt b/examples/numpy-test-app/requirements.txt new file mode 100644 index 00000000..a3b79fff --- /dev/null +++ b/examples/numpy-test-app/requirements.txt @@ -0,0 +1,6 @@ +gunicorn>=20.0.0; python_version >= '3.5' +# Numpy 1.23.0 no longer builds from source on very old GCC toolchains +numpy<1.23.0; python_version >= '3.6' and python_version < '3.11' + +# numpy 1.23.5 is the first one with wheels for Python 3.11 +numpy>1.23.5; python_version >= "3.11" diff --git a/examples/numpy-test-app/wsgi.py b/examples/numpy-test-app/wsgi.py new file mode 100644 index 00000000..6893f4a2 --- /dev/null +++ b/examples/numpy-test-app/wsgi.py @@ -0,0 +1,7 @@ +import numpy + +def application(environ, start_response): + start_response('200 OK', [('Content-Type','text/plain')]) + matrix = numpy.array([[1,2,3],[6,5,4],[7,8,8]]) + matrix.dot(numpy.linalg.inv(matrix)) + return [b"Hello World from numpy WSGI application!"] diff --git a/examples/pin-pipenv-version-test-app/.s2i/environment b/examples/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..530e67f1 --- /dev/null +++ b/examples/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2026.0.3 diff --git a/examples/pin-pipenv-version-test-app/app.sh b/examples/pin-pipenv-version-test-app/app.sh new file mode 100755 index 00000000..4334c760 --- /dev/null +++ b/examples/pin-pipenv-version-test-app/app.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/examples/pin-pipenv-version-test-app/requirements.txt b/examples/pin-pipenv-version-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/examples/pin-pipenv-version-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/examples/pin-pipenv-version-test-app/wsgi.py b/examples/pin-pipenv-version-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/examples/pin-pipenv-version-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/examples/pipenv-test-app/.gitignore b/examples/pipenv-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/pipenv-test-app/.s2i/environment b/examples/pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..4a4f7605 --- /dev/null +++ b/examples/pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +DISABLE_SETUP_PY_PROCESSING=true diff --git a/examples/pipenv-test-app/Pipfile b/examples/pipenv-test-app/Pipfile new file mode 100644 index 00000000..45aee705 --- /dev/null +++ b/examples/pipenv-test-app/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "3.14" diff --git a/examples/pipenv-test-app/Pipfile.lock b/examples/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/examples/pipenv-test-app/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/examples/pipenv-test-app/setup.py b/examples/pipenv-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/examples/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/examples/pipenv-test-app/testapp.py b/examples/pipenv-test-app/testapp.py new file mode 100644 index 00000000..fe200352 --- /dev/null +++ b/examples/pipenv-test-app/testapp.py @@ -0,0 +1,12 @@ +import sys +import requests + + +def application(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + # Python 3.12 needed requests version bump + if sys.version_info.major == 3 and sys.version_info.minor >= 12: + assert requests.__version__ == '2.28.2' + else: + assert requests.__version__ == '2.20.0' + return [b"Hello from gunicorn WSGI application!"] diff --git a/examples/poetry-src-layout-test-app/.s2i/environment b/examples/poetry-src-layout-test-app/.s2i/environment new file mode 100644 index 00000000..e6e3d481 --- /dev/null +++ b/examples/poetry-src-layout-test-app/.s2i/environment @@ -0,0 +1 @@ +APP_MODULE="poetry_src_layout:application" diff --git a/examples/poetry-src-layout-test-app/poetry.lock b/examples/poetry-src-layout-test-app/poetry.lock new file mode 100644 index 00000000..ba15cadf --- /dev/null +++ b/examples/poetry-src-layout-test-app/poetry.lock @@ -0,0 +1,24 @@ +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.6" +content-hash = "e0a7b0bc1c0f3d7d6d5781929434419af5258f2e5f831782dc5e02af1e7010d5" + +[metadata.files] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] diff --git a/examples/poetry-src-layout-test-app/pyproject.toml b/examples/poetry-src-layout-test-app/pyproject.toml new file mode 100644 index 00000000..09d17324 --- /dev/null +++ b/examples/poetry-src-layout-test-app/pyproject.toml @@ -0,0 +1,14 @@ +[tool.poetry] +name = "poetry-src-layout" +version = "0.1.0" +description = "" +authors = ["Your Name "] +packages = [{include = "poetry_src_layout", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.6" +gunicorn = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/examples/poetry-src-layout-test-app/src/poetry_src_layout/__init__.py b/examples/poetry-src-layout-test-app/src/poetry_src_layout/__init__.py new file mode 100644 index 00000000..1904a2f9 --- /dev/null +++ b/examples/poetry-src-layout-test-app/src/poetry_src_layout/__init__.py @@ -0,0 +1,3 @@ +def application(environ, start_response): + start_response('200 OK', [('Content-Type','text/plain')]) + return [b"Hello from gunicorn WSGI application!"] diff --git a/examples/pyuwsgi-pipenv-test-app/.s2i/environment b/examples/pyuwsgi-pipenv-test-app/.s2i/environment new file mode 100644 index 00000000..530e67f1 --- /dev/null +++ b/examples/pyuwsgi-pipenv-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=1 +PIN_PIPENV_VERSION=2026.0.3 diff --git a/examples/pyuwsgi-pipenv-test-app/app.sh b/examples/pyuwsgi-pipenv-test-app/app.sh new file mode 100755 index 00000000..3a2680f2 --- /dev/null +++ b/examples/pyuwsgi-pipenv-test-app/app.sh @@ -0,0 +1,23 @@ +#!/bin/bash +source .s2i/environment + +echo "Testing PIN_PIPENV_VERSION (set in .s2i/environment) ..." +pipenv_version=`pipenv --version` +expected="pipenv, version $PIN_PIPENV_VERSION" +if [ "$pipenv_version" != "$expected" ]; then + echo "ERROR: pipenv version is different than expected." + echo "Expected: ${expected}" + echo "Actual: ${pipenv_version}" + 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/examples/pyuwsgi-pipenv-test-app/requirements.txt b/examples/pyuwsgi-pipenv-test-app/requirements.txt new file mode 100644 index 00000000..932ba1d6 --- /dev/null +++ b/examples/pyuwsgi-pipenv-test-app/requirements.txt @@ -0,0 +1,2 @@ +pyuwsgi # this is just a copy of uwsgi project but it has wheels on PyPI +Flask diff --git a/examples/pyuwsgi-pipenv-test-app/wsgi.py b/examples/pyuwsgi-pipenv-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/examples/pyuwsgi-pipenv-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/examples/setup-cfg-test-app/pyproject.toml b/examples/setup-cfg-test-app/pyproject.toml new file mode 100644 index 00000000..374b58cb --- /dev/null +++ b/examples/setup-cfg-test-app/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/examples/setup-cfg-test-app/setup.cfg b/examples/setup-cfg-test-app/setup.cfg new file mode 100644 index 00000000..a8bedfdd --- /dev/null +++ b/examples/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/examples/setup-cfg-test-app/wsgi.py b/examples/setup-cfg-test-app/wsgi.py new file mode 100644 index 00000000..09fee1b7 --- /dev/null +++ b/examples/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/examples/setup-requirements-test-app/.gitignore b/examples/setup-requirements-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/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/examples/setup-requirements-test-app/requirements.txt b/examples/setup-requirements-test-app/requirements.txt new file mode 100644 index 00000000..40b7e536 --- /dev/null +++ b/examples/setup-requirements-test-app/requirements.txt @@ -0,0 +1,4 @@ +-e . +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' +setuptools \ No newline at end of file diff --git a/examples/setup-requirements-test-app/setup.py b/examples/setup-requirements-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/examples/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/examples/setup-requirements-test-app/testapp.py b/examples/setup-requirements-test-app/testapp.py new file mode 100644 index 00000000..09fee1b7 --- /dev/null +++ b/examples/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/examples/setup-test-app/.gitignore b/examples/setup-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/setup-test-app/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/examples/setup-test-app/requirements.txt b/examples/setup-test-app/requirements.txt new file mode 100644 index 00000000..37dfec99 --- /dev/null +++ b/examples/setup-test-app/requirements.txt @@ -0,0 +1,3 @@ +gunicorn<20.0.0; python_version < '3.5' +gunicorn>=20.0.0; python_version >= '3.5' +setuptools \ No newline at end of file diff --git a/examples/setup-test-app/setup.py b/examples/setup-test-app/setup.py new file mode 100644 index 00000000..e3134094 --- /dev/null +++ b/examples/setup-test-app/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + + +setup ( + name = "testapp", + version = "0.1", + description = "Example application to be deployed.", + packages = find_packages(), + install_requires = ["gunicorn"], +) diff --git a/examples/setup-test-app/testapp.py b/examples/setup-test-app/testapp.py new file mode 100644 index 00000000..09fee1b7 --- /dev/null +++ b/examples/setup-test-app/testapp.py @@ -0,0 +1,4 @@ + +def application(environ, start_response): + start_response('200 OK', [('Content-Type','text/plain')]) + return [b"Hello from gunicorn WSGI application!"] diff --git a/examples/standalone-custom-pypi-index-test-app/.gitignore b/examples/standalone-custom-pypi-index-test-app/.gitignore new file mode 100644 index 00000000..ba746605 --- /dev/null +++ b/examples/standalone-custom-pypi-index-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/examples/standalone-custom-pypi-index-test-app/.s2i/environment b/examples/standalone-custom-pypi-index-test-app/.s2i/environment new file mode 100644 index 00000000..25d96d59 --- /dev/null +++ b/examples/standalone-custom-pypi-index-test-app/.s2i/environment @@ -0,0 +1,2 @@ +PIP_INDEX_URL=https://test.pypi.org/simple/ +DISABLE_SETUP_PY_PROCESSING=true diff --git a/examples/standalone-custom-pypi-index-test-app/app.py b/examples/standalone-custom-pypi-index-test-app/app.py new file mode 100644 index 00000000..a942e3bd --- /dev/null +++ b/examples/standalone-custom-pypi-index-test-app/app.py @@ -0,0 +1,23 @@ +import http.server +import signal +import socketserver +import sys + +PORT = 8080 + +def sigterm_handler(signum, frame): + print("SIGTERM received, performing cleanup...") + sys.exit(0) + +signal.signal(signal.SIGTERM, sigterm_handler) + +class SimpleHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(b"Hello, World!") + +with socketserver.TCPServer(('', PORT), SimpleHTTPRequestHandler) as httpd: + print("serving at port", PORT) + httpd.serve_forever() diff --git a/examples/standalone-custom-pypi-index-test-app/app.sh b/examples/standalone-custom-pypi-index-test-app/app.sh new file mode 100755 index 00000000..bb4b5cd4 --- /dev/null +++ b/examples/standalone-custom-pypi-index-test-app/app.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +echo "Testing pello package installed version..." + +if pip freeze | grep Pello==1.0.3; then + echo "Pello 1.0.3 is installed, starting webserver..." + exec python app.py +else + echo "Pello 1.0.3 wasn't installed, exiting..." + exit 1 +fi diff --git a/examples/standalone-custom-pypi-index-test-app/requirements.txt b/examples/standalone-custom-pypi-index-test-app/requirements.txt new file mode 100644 index 00000000..e9ad401d --- /dev/null +++ b/examples/standalone-custom-pypi-index-test-app/requirements.txt @@ -0,0 +1 @@ +pello diff --git a/3.3/test/standalone-test-app/app.py b/examples/standalone-test-app/app.py similarity index 87% rename from 3.3/test/standalone-test-app/app.py rename to examples/standalone-test-app/app.py index d786ee79..a3403e5d 100644 --- a/3.3/test/standalone-test-app/app.py +++ b/examples/standalone-test-app/app.py @@ -1,10 +1,13 @@ import os +import time from gunicorn.app.base import BaseApplication -from gunicorn.six import iteritems +from six import iteritems +print('LOADING MODULE %s' % __file__) def wsgi_handler(environ, start_response): + print('HANDLE REQUEST %s' % time.time()) start_response('200 OK', [('Content-Type','text/html')]) return [b"Hello World from standalone WSGI application!"] diff --git a/examples/standalone-test-app/requirements.txt b/examples/standalone-test-app/requirements.txt new file mode 100644 index 00000000..d1f415e8 --- /dev/null +++ b/examples/standalone-test-app/requirements.txt @@ -0,0 +1,3 @@ +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/examples/uwsgi-test-app/.s2i/environment b/examples/uwsgi-test-app/.s2i/environment new file mode 100644 index 00000000..c1761606 --- /dev/null +++ b/examples/uwsgi-test-app/.s2i/environment @@ -0,0 +1 @@ +UPGRADE_PIP_TO_LATEST=1 diff --git a/examples/uwsgi-test-app/app.sh b/examples/uwsgi-test-app/app.sh new file mode 100755 index 00000000..bc86dbca --- /dev/null +++ b/examples/uwsgi-test-app/app.sh @@ -0,0 +1,42 @@ +#!/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 that the virtual environment is active ..." +if ! export | grep -q 'VIRTUAL_ENV="/opt/app-root"'; then + echo "VIRTUAL_ENV is not set properly!" + export + 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 $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 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/examples/uwsgi-test-app/requirements.txt b/examples/uwsgi-test-app/requirements.txt new file mode 100644 index 00000000..66d42051 --- /dev/null +++ b/examples/uwsgi-test-app/requirements.txt @@ -0,0 +1,2 @@ +uWSGI +Flask diff --git a/examples/uwsgi-test-app/wsgi.py b/examples/uwsgi-test-app/wsgi.py new file mode 100644 index 00000000..96e1a614 --- /dev/null +++ b/examples/uwsgi-test-app/wsgi.py @@ -0,0 +1,9 @@ +from flask import Flask +application = Flask(__name__) + +@application.route('/') +def hello(): + return b'Hello World from uWSGI hosted WSGI application!' + +if __name__ == '__main__': + application.run() diff --git a/hack/build.sh b/hack/build.sh deleted file mode 100755 index c62825b1..00000000 --- a/hack/build.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -e -# This script is used to build, test and squash the OpenShift Docker images. -# -# Name of resulting image will be: 'NAMESPACE/BASE_IMAGE_NAME-VERSION-OS'. -# -# BASE_IMAGE_NAME - Usually name of the main component within container. -# OS - Specifies distribution - "rhel7" or "centos7" -# VERSION - Specifies the image version - (must match with subdirectory in repo) -# TEST_MODE - If set, build a candidate image and test it -# TAG_ON_SUCCESS - If set, tested image will be re-tagged as a non-candidate -# image, if the tests pass. -# VERSIONS - Must be set to a list with possible versions (subdirectories) -# OPENSHIFT_NAMESPACES - Which of available versions (subdirectories) should be -# put into openshift/ namespace. - -OS=${1-$OS} -VERSION=${2-$VERSION} - -DOCKERFILE_PATH="" - -test -z "$BASE_IMAGE_NAME" && { - BASE_DIR_NAME=$(echo $(basename `pwd`) | sed -e 's/-[0-9]*$//g') - BASE_IMAGE_NAME="${BASE_DIR_NAME#sti-}" -} - -NAMESPACE="openshift/" - -# Cleanup the temporary Dockerfile created by docker build with version -trap "rm -f ${DOCKERFILE_PATH}.version" SIGINT SIGQUIT EXIT - -# Perform docker build but append the LABEL with GIT commit id at the end -function docker_build_with_version { - local dockerfile="$1" - # Use perl here to make this compatible with OSX - DOCKERFILE_PATH=$(perl -MCwd -e 'print Cwd::abs_path shift' $dockerfile) - cp ${DOCKERFILE_PATH} "${DOCKERFILE_PATH}.version" - git_version=$(git rev-parse HEAD) - echo "LABEL io.openshift.builder-version=\"${git_version}\"" >> "${dockerfile}.version" - docker build -t ${IMAGE_NAME} -f "${dockerfile}.version" . - if [[ "${SKIP_SQUASH}" != "1" ]]; then - squash "${dockerfile}.version" - fi - rm -f "${DOCKERFILE_PATH}.version" -} - -# Install the docker squashing tool[1] and squash the result image -# [1] https://github.com/goldmann/docker-scripts -function squash { - # FIXME: We have to use the exact versions here to avoid Docker client - # compatibility issues - easy_install -q --user docker_py==1.6.0 docker-scripts==0.4.4 - base=$(awk '/^FROM/{print $2}' $1) - ${HOME}/.local/bin/docker-scripts squash -f $base ${IMAGE_NAME} -} - -# Versions are stored in subdirectories. You can specify VERSION variable -# to build just one single version. By default we build all versions -dirs=${VERSION:-$VERSIONS} - -for dir in ${dirs}; do - case " $OPENSHIFT_NAMESPACES " in - *\ ${dir}\ *) ;; - *) - if [ "${OS}" == "centos7" ]; then - NAMESPACE="centos/" - else - NAMESPACE="rhscl/" - fi - esac - - IMAGE_NAME="${NAMESPACE}${BASE_IMAGE_NAME}-${dir//./}-${OS}" - - if [[ -v TEST_MODE ]]; then - IMAGE_NAME+="-candidate" - fi - - echo "-> Building ${IMAGE_NAME} ..." - - pushd ${dir} > /dev/null - if [ "$OS" == "rhel7" -o "$OS" == "rhel7-candidate" ]; then - docker_build_with_version Dockerfile.rhel7 - else - docker_build_with_version Dockerfile - fi - - if [[ -v TEST_MODE ]]; then - IMAGE_NAME=${IMAGE_NAME} test/run - - if [[ $? -eq 0 ]] && [[ "${TAG_ON_SUCCESS}" == "true" ]]; then - echo "-> Re-tagging ${IMAGE_NAME} image to ${IMAGE_NAME%"-candidate"}" - docker tag -f $IMAGE_NAME ${IMAGE_NAME%"-candidate"} - fi - fi - - popd > /dev/null -done diff --git a/hack/common.mk b/hack/common.mk deleted file mode 100644 index bcca4098..00000000 --- a/hack/common.mk +++ /dev/null @@ -1,25 +0,0 @@ -SKIP_SQUASH?=0 - -build = hack/build.sh - -ifeq ($(TARGET),rhel7) - OS := rhel7 -else - OS := centos7 -endif - -script_env = \ - SKIP_SQUASH=$(SKIP_SQUASH) \ - VERSIONS="$(VERSIONS)" \ - OS=$(OS) \ - VERSION=$(VERSION) \ - BASE_IMAGE_NAME=$(BASE_IMAGE_NAME) \ - OPENSHIFT_NAMESPACES="$(OPENSHIFT_NAMESPACES)" - -.PHONY: build -build: - $(script_env) $(build) - -.PHONY: test -test: - $(script_env) TAG_ON_SUCCESS=$(TAG_ON_SUCCESS) TEST_MODE=true $(build) diff --git a/imagestreams/imagestreams.yaml b/imagestreams/imagestreams.yaml new file mode 100644 index 00000000..9c1a5144 --- /dev/null +++ b/imagestreams/imagestreams.yaml @@ -0,0 +1,48 @@ +--- +- name: python + pretty_name: Python + sample_repo: https://github.com/sclorg/django-ex.git + category: builder + description: >- + Build and run Python APP_VERSION applications on DISTRO_NAME. + For more information about using this builder image, + including OpenShift considerations, see + https://github.com/sclorg/s2i-python-container/blob/master/APP_VERSION/README.md. + imagestream_files: + - filename: python-centos.json + latest: "3.12-ubi9" + distros: + - name: UBI 8 + app_versions: ["3.6", "3.9"] + + - name: UBI 9 + app_versions: ["3.9", "3.12-minimal"] + + - name: UBI 10 + app_versions: ["3.12-minimal"] + + - filename: python-rhel.json + latest: "3.12-ubi8" + distros: + - name: UBI 8 + app_versions: ["3.6", "3.9"] + + - name: UBI 9 + app_versions: ["3.9", "3.12-minimal"] + + - name: UBI 10 + app_versions: ["3.12-minimal"] + + - filename: python-rhel-aarch64.json + latest: "3.12-ubi8" + distros: + - name: UBI 8 + app_versions: ["3.6", "3.9"] + + - name: UBI 9 + app_versions: ["3.9", "3.12-minimal"] + + - name: UBI 10 + app_versions: ["3.12-minimal"] + +... diff --git a/imagestreams/python-centos.json b/imagestreams/python-centos.json new file mode 100644 index 00000000..137fc78e --- /dev/null +++ b/imagestreams/python-centos.json @@ -0,0 +1,147 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "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", + "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": "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", + "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.9-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 9. 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", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi9/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi9/python-312:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi9/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi10", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 10)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 10. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.access.redhat.com/ubi10/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python 3.12 (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version available on OpenShift, including major version updates.\n", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.12-ubi9" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/imagestreams/python-rhel-aarch64.json b/imagestreams/python-rhel-aarch64.json new file mode 100644 index 00000000..111b7444 --- /dev/null +++ b/imagestreams/python-rhel-aarch64.json @@ -0,0 +1,147 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "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", + "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": "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", + "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.9-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 9. 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", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-312:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi10", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 10)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 10. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi10/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python 3.12 (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 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.12/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version available on OpenShift, including major version updates.\n", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.12-ubi8" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/imagestreams/python-rhel.json b/imagestreams/python-rhel.json new file mode 100644 index 00000000..111b7444 --- /dev/null +++ b/imagestreams/python-rhel.json @@ -0,0 +1,147 @@ +{ + "kind": "ImageStream", + "apiVersion": "image.openshift.io/v1", + "metadata": { + "name": "python", + "annotations": { + "openshift.io/display-name": "Python" + } + }, + "spec": { + "tags": [ + { + "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", + "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": "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", + "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.9-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.9 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.9 applications on UBI 9. 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", + "version": "3.9", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-39:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12 (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-312:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi9", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 9)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 9. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi9/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "3.12-minimal-ubi10", + "annotations": { + "openshift.io/display-name": "Python 3.12-minimal (UBI 10)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12-minimal applications on UBI 10. For more information about using this builder image, including OpenShift considerations, see https://github.com/sclorg/s2i-python-container/blob/master/3.12-minimal/README.md.", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12-minimal", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "DockerImage", + "name": "registry.redhat.io/ubi10/python-312-minimal:latest" + }, + "referencePolicy": { + "type": "Local" + } + }, + { + "name": "latest", + "annotations": { + "openshift.io/display-name": "Python 3.12 (Latest)", + "openshift.io/provider-display-name": "Red Hat, Inc.", + "description": "Build and run Python 3.12 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.12/README.md.\n\nWARNING: By selecting this tag, your application will automatically update to use the latest version available on OpenShift, including major version updates.\n", + "iconClass": "icon-python", + "tags": "builder,python", + "version": "3.12", + "sampleRepo": "https://github.com/sclorg/django-ex.git" + }, + "from": { + "kind": "ImageStreamTag", + "name": "3.12-ubi8" + }, + "referencePolicy": { + "type": "Local" + } + } + ] + } +} diff --git a/manifest-minimal.yml b/manifest-minimal.yml new file mode 100644 index 00000000..8eec6f5c --- /dev/null +++ b/manifest-minimal.yml @@ -0,0 +1,228 @@ +# Files containing distgen directives +DISTGEN_RULES: + - src: src/README-minimal.md + dest: README.md + + + - src: src/s2i/bin/assemble + dest: s2i/bin/assemble + mode: "0755" + + - src: src/s2i/bin/usage + dest: s2i/bin/usage + mode: "0755" + + - src: src/s2i/bin/run + dest: s2i/bin/run + mode: "0755" + + - src: test/run + dest: test/run + mode: "0755" + + - src: test/run-pytest + dest: test/run-pytest + mode: "0755" + + - src: src/test/pipenv-test-app/Pipfile + dest: test/pipenv-test-app/Pipfile + + - src: src/test/pipenv-test-app/Pipfile.lock + dest: test/pipenv-test-app/Pipfile.lock + + - src: src/test/pipenv-test-app/Pipfile + dest: test/micropipenv-test-app/Pipfile + + - src: src/test/pipenv-test-app/Pipfile.lock + dest: test/micropipenv-test-app/Pipfile.lock + + - src: src/test/from-dockerfile/uwsgi.Dockerfile.tpl + dest: test/from-dockerfile/uwsgi.Dockerfile.tpl + + - src: src/test/pin-pipenv-version-test-app/.s2i/environment + dest: test/pyuwsgi-pipenv-test-app/.s2i/environment + + +# Files containing distgen directives, which are used for each +# (distro, version) combination not excluded in multispec +DISTGEN_MULTI_RULES: + - src: src/Dockerfile-minimal.template + dest: Dockerfile.rhel8 + + - src: src/Dockerfile-minimal.template + dest: Dockerfile.rhel9 + + - src: src/Dockerfile-minimal.template + dest: Dockerfile.rhel10 + + - src: src/Dockerfile-minimal.template + dest: Dockerfile.fedora + + - src: src/Dockerfile-minimal.template + dest: Dockerfile.c9s + + - src: src/Dockerfile-minimal.template + dest: Dockerfile.c10s + +# Files to copy +COPY_RULES: + - src: src/root/opt/app-root/etc/generate_container_user + dest: root/opt/app-root/etc/generate_container_user + + - src: src/s2i/bin/init-wrapper + dest: s2i/bin/init-wrapper + mode: "0755" + + - src: examples/pipenv-test-app/testapp.py + dest: test/pipenv-test-app/testapp.py + + - src: examples/pipenv-test-app/setup.py + dest: test/pipenv-test-app/setup.py + + - src: examples/pipenv-test-app/.s2i/environment + dest: test/pipenv-test-app/.s2i/environment + + - src: examples/pipenv-test-app/.gitignore + dest: test/pipenv-test-app/.gitignore + + - src: examples/micropipenv-test-app/testapp.py + dest: test/micropipenv-test-app/testapp.py + + - src: examples/micropipenv-test-app/setup.py + dest: test/micropipenv-test-app/setup.py + + - src: examples/micropipenv-test-app/.s2i/environment + dest: test/micropipenv-test-app/.s2i/environment + + - src: examples/micropipenv-test-app/.gitignore + dest: test/micropipenv-test-app/.gitignore + + - src: common/shared-scripts/core/usr/bin/cgroup-limits + dest: root/usr/bin/cgroup-limits + mode: "0755" + + - src: common/shared-scripts/core/usr/bin/fix-permissions + dest: root/usr/bin/fix-permissions + mode: "0755" + + - src: common/shared-scripts/core/usr/bin/rpm-file-permissions + dest: root/usr/bin/rpm-file-permissions + mode: "0755" + + - src: examples/pyuwsgi-pipenv-test-app/requirements.txt + dest: test/pyuwsgi-pipenv-test-app/requirements.txt + + - src: examples/pyuwsgi-pipenv-test-app/wsgi.py + dest: test/pyuwsgi-pipenv-test-app/wsgi.py + + - src: examples/pyuwsgi-pipenv-test-app/app.sh + dest: test/pyuwsgi-pipenv-test-app/app.sh + +# Symbolic links +# This section is the last one on purpose because the generator.py +# does not allow dead symlinks. +SYMLINK_RULES: + - src: ../../examples/app-home-test-app + dest: test/app-home-test-app + + - src: ../../examples/app-module-test-app + dest: test/app-module-test-app + + - src: ../../examples/django-test-app + dest: test/django-test-app + + - src: ../../examples/django-different-port-test-app + dest: test/django-different-port-test-app + + - src: ../../examples/gunicorn-different-port-test-app + dest: test/gunicorn-different-port-test-app + + - src: ../../examples/gunicorn-config-different-port-test-app + dest: test/gunicorn-config-different-port-test-app + + - src: ../../examples/gunicorn-python-configfile-different-port-test-app + dest: test/gunicorn-python-configfile-different-port-test-app + + - src: ../../examples/locale-test-app + dest: test/locale-test-app + + - src: ../../examples/micropipenv-requirements-test-app + dest: test/micropipenv-requirements-test-app + + - src: ../../examples/uwsgi-test-app + dest: test/uwsgi-test-app + + - src: ../../examples/numpy-test-app + dest: test/numpy-test-app + + - src: ../../examples/poetry-src-layout-test-app + dest: test/poetry-src-layout-test-app + + - src: ../../examples/setup-requirements-test-app + dest: test/setup-requirements-test-app + + - src: ../../examples/setup-test-app + dest: test/setup-test-app + + - src: ../../examples/setup-cfg-test-app + dest: test/setup-cfg-test-app + + - src: ../../examples/standalone-test-app + dest: test/standalone-test-app + + - src: ../../examples/standalone-custom-pypi-index-test-app + dest: test/standalone-custom-pypi-index-test-app + + - src: ../../src/test/pipenv-and-micropipenv-should-fail-test-app + dest: test/pipenv-and-micropipenv-should-fail-test-app + + - src: ../../../src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl + dest: test/from-dockerfile/mod_wsgi.Dockerfile.tpl + + - src: ../../test/run-openshift-pytest + dest: test/run-openshift-pytest + + - src: ../../test/test_container_application.py + dest: test/test_container_application.py + + - src: ../../test/test_container_basics.py + dest: test/test_container_basics.py + + - src: ../../test/test_ocp_deploy_templates.py + dest: test/test_ocp_deploy_templates.py + + - src: ../../test/test_ocp_imagestreams_quickstart.py + dest: test/test_ocp_imagestreams_quickstart.py + + - src: ../../test/test_ocp_python_ex_standalone.py + dest: test/test_ocp_python_ex_standalone.py + + - src: ../../test/test_ocp_python_ex_template.py + dest: test/test_ocp_python_ex_template.py + + - src: ../../test/test_ocp_helm_python_django_app.py + dest: test/test_ocp_helm_python_django_app.py + + - src: ../../test/test_ocp_helm_python_django_psql_persistent.py + dest: test/test_ocp_helm_python_django_psql_persistent.py + + - src: ../../test/test_ocp_helm_python_imagestreams.py + dest: test/test_ocp_helm_python_imagestreams.py + + - src: ../../test/conftest.py + dest: test/conftest.py + + - src: ../../test/__init__.py + dest: test/__init__.py + + - src: ../../common/test-lib.sh + dest: test/test-lib.sh + + - src: ../../imagestreams + dest: test/imagestreams + + - src: ../../common/check_imagestreams.py + dest: test/check_imagestreams.py + + - src: ../../../src/root/opt/wheels/ + dest: root/opt/wheels diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 00000000..4cb6b4de --- /dev/null +++ b/manifest.yml @@ -0,0 +1,227 @@ +# Files containing distgen directives +DISTGEN_RULES: + - src: src/README.md + dest: README.md + + + - src: src/s2i/bin/assemble + dest: s2i/bin/assemble + mode: "0755" + + - src: src/s2i/bin/usage + dest: s2i/bin/usage + mode: "0755" + + - src: src/s2i/bin/run + dest: s2i/bin/run + mode: "0755" + + - src: test/run + dest: test/run + mode: "0755" + + - src: test/run-pytest + dest: test/run-pytest + mode: "0755" + + - src: src/test/pipenv-test-app/Pipfile + dest: test/pipenv-test-app/Pipfile + + - src: src/test/pipenv-test-app/Pipfile.lock + dest: test/pipenv-test-app/Pipfile.lock + + - src: src/test/pipenv-test-app/Pipfile + dest: test/micropipenv-test-app/Pipfile + + - src: src/test/pipenv-test-app/Pipfile.lock + dest: test/micropipenv-test-app/Pipfile.lock + + - src: src/test/from-dockerfile/Dockerfile_no_s2i.tpl + dest: test/from-dockerfile/Dockerfile_no_s2i.tpl + + - src: src/test/pin-pipenv-version-test-app/.s2i/environment + dest: test/pin-pipenv-version-test-app/.s2i/environment + + - src: src/test/pin-pipenv-version-test-app/.s2i/environment + dest: test/pyuwsgi-pipenv-test-app/.s2i/environment + +# Files containing distgen directives, which are used for each +# (distro, version) combination not excluded in multispec +DISTGEN_MULTI_RULES: + - src: src/Dockerfile.template + dest: Dockerfile.rhel8 + + - src: src/Dockerfile.template + dest: Dockerfile.rhel9 + + - src: src/Dockerfile.template + dest: Dockerfile.fedora + + - src: src/Dockerfile.template + dest: Dockerfile.c9s + + - src: src/Dockerfile.template + dest: Dockerfile.c10s + +# Files to copy +COPY_RULES: + - src: src/root/opt/app-root/etc/generate_container_user + dest: root/opt/app-root/etc/generate_container_user + + - src: src/s2i/bin/init-wrapper + dest: s2i/bin/init-wrapper + mode: "0755" + + - src: examples/pipenv-test-app/testapp.py + dest: test/pipenv-test-app/testapp.py + + - src: examples/pipenv-test-app/setup.py + dest: test/pipenv-test-app/setup.py + + - src: examples/pipenv-test-app/.s2i/environment + dest: test/pipenv-test-app/.s2i/environment + + - src: examples/pipenv-test-app/.gitignore + dest: test/pipenv-test-app/.gitignore + + - src: examples/micropipenv-test-app/testapp.py + dest: test/micropipenv-test-app/testapp.py + + - src: examples/micropipenv-test-app/setup.py + dest: test/micropipenv-test-app/setup.py + + - src: examples/micropipenv-test-app/.s2i/environment + dest: test/micropipenv-test-app/.s2i/environment + + - src: examples/micropipenv-test-app/.gitignore + dest: test/micropipenv-test-app/.gitignore + + - src: examples/pin-pipenv-version-test-app/requirements.txt + dest: test/pin-pipenv-version-test-app/requirements.txt + + - src: examples/pin-pipenv-version-test-app/wsgi.py + dest: test/pin-pipenv-version-test-app/wsgi.py + + - src: examples/pin-pipenv-version-test-app/app.sh + dest: test/pin-pipenv-version-test-app/app.sh + + - src: examples/pyuwsgi-pipenv-test-app/requirements.txt + dest: test/pyuwsgi-pipenv-test-app/requirements.txt + + - src: examples/pyuwsgi-pipenv-test-app/wsgi.py + dest: test/pyuwsgi-pipenv-test-app/wsgi.py + + - src: examples/pyuwsgi-pipenv-test-app/app.sh + dest: test/pyuwsgi-pipenv-test-app/app.sh + +# Symbolic links +# This section is the last one on purpose because the generator.py +# does not allow dead symlinks. +SYMLINK_RULES: + - src: ../../examples/app-home-test-app + dest: test/app-home-test-app + + - src: ../../examples/app-module-test-app + dest: test/app-module-test-app + + - src: ../../examples/django-test-app + dest: test/django-test-app + + - src: ../../examples/django-different-port-test-app + dest: test/django-different-port-test-app + + - src: ../../examples/gunicorn-different-port-test-app + dest: test/gunicorn-different-port-test-app + + - src: ../../examples/gunicorn-config-different-port-test-app + dest: test/gunicorn-config-different-port-test-app + + - src: ../../examples/gunicorn-python-configfile-different-port-test-app + dest: test/gunicorn-python-configfile-different-port-test-app + + - src: ../../examples/locale-test-app + dest: test/locale-test-app + + - src: ../../examples/micropipenv-requirements-test-app + dest: test/micropipenv-requirements-test-app + + - src: ../../examples/mod-wsgi-test-app + dest: test/mod-wsgi-test-app + + - src: ../../examples/npm-virtualenv-uwsgi-test-app + dest: test/npm-virtualenv-uwsgi-test-app + + - src: ../../examples/numpy-test-app + dest: test/numpy-test-app + + - src: ../../examples/poetry-src-layout-test-app + dest: test/poetry-src-layout-test-app + + - src: ../../examples/setup-requirements-test-app + dest: test/setup-requirements-test-app + + - src: ../../examples/setup-test-app + dest: test/setup-test-app + + - src: ../../examples/setup-cfg-test-app + dest: test/setup-cfg-test-app + + - src: ../../examples/standalone-test-app + dest: test/standalone-test-app + + - src: ../../examples/standalone-custom-pypi-index-test-app + dest: test/standalone-custom-pypi-index-test-app + + - src: ../../src/test/pipenv-and-micropipenv-should-fail-test-app + dest: test/pipenv-and-micropipenv-should-fail-test-app + + - src: ../../../src/test/from-dockerfile/Dockerfile.tpl + dest: test/from-dockerfile/Dockerfile.tpl + + - src: ../../test/run-openshift-pytest + dest: test/run-openshift-pytest + + - src: ../../test/test_container_application.py + dest: test/test_container_application.py + + - src: ../../test/test_container_basics.py + dest: test/test_container_basics.py + + - src: ../../test/test_ocp_deploy_templates.py + dest: test/test_ocp_deploy_templates.py + + - src: ../../test/test_ocp_imagestreams_quickstart.py + dest: test/test_ocp_imagestreams_quickstart.py + + - src: ../../test/test_ocp_python_ex_standalone.py + dest: test/test_ocp_python_ex_standalone.py + + - src: ../../test/test_ocp_python_ex_template.py + dest: test/test_ocp_python_ex_template.py + + - src: ../../test/test_ocp_helm_python_django_app.py + dest: test/test_ocp_helm_python_django_app.py + + - src: ../../test/test_ocp_helm_python_django_psql_persistent.py + dest: test/test_ocp_helm_python_django_psql_persistent.py + + - src: ../../test/test_ocp_helm_python_imagestreams.py + dest: test/test_ocp_helm_python_imagestreams.py + + - src: ../../test/conftest.py + dest: test/conftest.py + + - src: ../../test/__init__.py + dest: test/__init__.py + + - src: ../../common/test-lib.sh + dest: test/test-lib.sh + + - src: ../../imagestreams + dest: test/imagestreams + + - src: ../../common/check_imagestreams.py + dest: test/check_imagestreams.py + + - src: ../../../src/root/opt/wheels/ + dest: root/opt/wheels diff --git a/specs/multispec.yml b/specs/multispec.yml new file mode 100644 index 00000000..2486f3e0 --- /dev/null +++ b/specs/multispec.yml @@ -0,0 +1,271 @@ +version: 1 + +specs: + distroinfo: + rhel8: + distros: + - rhel-8-x86_64 + el_version: "8" + minimal_image: "ubi8/ubi-minimal:latest" + s2i_base: ubi8/s2i-base + img_tag: "1" + org: "ubi8" + prod: "rhel8" + logos: "redhat-logos-httpd" + preinstall_cmd: ['yum -y module enable {{ spec.module_stream }} httpd:2.4'] + python_pkgs: [] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'mod_ssl', + 'mod_auth_gssapi', 'mod_ldap', 'mod_session', + 'atlas-devel', 'gcc-gfortran', 'libffi-devel', 'libtool-ltdl', 'enchant', + 'krb5-devel'] + extra_pkgs: + "3.6": ['python36', 'python36-devel', '{{ spec.pkg_prefix }}-virtualenv', + '{{ spec.pkg_prefix }}-setuptools', '{{ spec.pkg_prefix }}-pip'] + "default": ['{{ spec.pkg_prefix }}', '{{ spec.pkg_prefix }}-devel', + '{{ spec.pkg_prefix }}-setuptools', '{{ spec.pkg_prefix }}-pip'] + "3.12": ['{{ spec.pkg_prefix }}', '{{ spec.pkg_prefix }}-devel', + '{{ spec.pkg_prefix }}-setuptools', '{{ spec.pkg_prefix }}-pip'] + ubi_versions: ['3.6', '3.9', '3.12'] + + rhel9: + distros: + - rhel-9-x86_64 + el_version: "9" + minimal_image: "ubi9/ubi-minimal:latest" + s2i_base: ubi9/s2i-base + img_tag: "1" + org: "ubi9" + prod: "rhel9" + logos: "redhat-logos-httpd" + python_pkgs: [] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'mod_ssl', + 'mod_auth_gssapi', 'mod_ldap', 'mod_session', + 'atlas-devel', 'gcc-gfortran', 'libffi-devel', 'libtool-ltdl', 'enchant', + 'krb5-devel'] + ubi_versions: ['3.9', '3.12', '3.14'] + extra_pkgs: + "3.9": ['python3', 'python3-devel', 'python3-setuptools', 'python3-pip'] + "3.12": ['python3.12', 'python3.12-devel', 'python3.12-pip'] + "3.14": ['python3.14', 'python3.14-devel', 'python3.14-pip'] + + rhel10: + distros: + - rhel-10-x86_64 + el_version: "10" + minimal_image: "ubi10-minimal:latest" + s2i_base: ubi10/s2i-base + img_tag: "1" + org: "ubi10" + prod: "rhel10" + logos: "redhat-logos-httpd" + python_pkgs: [] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'mod_ssl', + 'mod_auth_gssapi', 'mod_ldap', 'mod_session', + 'atlas-devel', 'gcc-gfortran', 'libffi-devel', 'libtool-ltdl', 'enchant', + 'krb5-devel'] + ubi_versions: ['3.12', '3.14'] + extra_pkgs: + "3.12": ['python3.12', 'python3.12-devel', 'python3.12-pip'] + "3.14": ['python3.14', 'python3.14-devel', 'python3.14-pip'] + + + c9s: + distros: + - centos-stream-9-x86_64 + el_version: "9" + minimal_image: "quay.io/centos/centos:stream9-minimal" + s2i_base: quay.io/sclorg/s2i-base-c9s + img_tag: "c9s" + org: "sclorg" + prod: "c9s" + python_pkgs: [] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'mod_ssl', + 'mod_auth_gssapi', 'mod_ldap', 'mod_session', + 'atlas-devel', 'gcc-gfortran', 'libffi-devel', 'libtool-ltdl', 'enchant', + 'krb5-devel'] + extra_pkgs: + "3.9": ['python3', 'python3-devel', 'python3-setuptools', 'python3-pip'] + "3.12": ['python3.12', 'python3.12-devel', 'python3.12-pip'] + "3.14": ['python3.14', 'python3.14-devel', 'python3.14-pip'] + + c10s: + distros: + - centos-stream-10-x86_64 + el_version: "10" + minimal_image: "quay.io/centos/centos:stream10-minimal" + s2i_base: quay.io/sclorg/s2i-base-c10s + img_tag: "c10s" + org: "sclorg" + prod: "c10s" + python_pkgs: [] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'mod_ssl', + 'mod_auth_gssapi', 'mod_ldap', 'mod_session', + 'gcc-gfortran', 'libffi-devel', 'libtool-ltdl', + 'krb5-devel'] + enable_epel_for: ["3.13", "3.13-minimal"] + extra_pkgs: + "3.12": ['python3', 'python3-devel', 'python3-setuptools', 'python3-pip'] + "3.13": ['python3.13', 'python3.13-devel', 'python3.13-pip'] + "3.14": ['python3.14', 'python3.14-devel', 'python3.14-pip'] + + fedora42: + distros: + - fedora-42-x86_64 + fedora_version: "42" + minimal_image: "quay.io/fedora/fedora-minimal:42" + s2i_base: quay.io/fedora/s2i-base + img_tag: "42" + org: "fedora" + python_pkgs: ['python3', 'python3-devel', 'python3-setuptools', 'python3-pip'] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'atlas-devel', 'gcc-gfortran', + 'libffi-devel', 'libtool-ltdl', 'enchant', 'redhat-rpm-config', + 'krb5-devel'] + + fedora43: + distros: + - fedora-43-x86_64 + fedora_version: "43" + minimal_image: "quay.io/fedora/fedora-minimal:43" + s2i_base: quay.io/fedora/s2i-base + img_tag: "43" + org: "fedora" + python_pkgs: ['python3', 'python3-devel', 'python3-setuptools', 'python3-pip'] + base_pkgs: ['nss_wrapper-libs', 'httpd', 'httpd-devel', 'atlas-devel', 'gcc-gfortran', + 'libffi-devel', 'libtool-ltdl', 'enchant', 'redhat-rpm-config', + 'krb5-devel'] + + version: + "3.14-minimal": + version: "3.14" + short_ver: "314" + minimal: true + base_img_version: "1" + python_img_version: "1" + # Python 3.14 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.14" + main_image: "quay.io/fedora/python-314-minimal" + + "3.14": + version: "3.14" + short_ver: "314" + minimal: false + base_img_version: "1" + # Python 3.14 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.14" + main_image: "quay.io/fedora/python-314" + + "3.13-minimal": + version: "3.13" + short_ver: "313" + minimal: true + base_img_version: "1" + python_img_version: "1" + # Python 3.13 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.13" + main_image: "quay.io/fedora/python-313-minimal" + + "3.13": + version: "3.13" + short_ver: "313" + minimal: false + base_img_version: "1" + # Python 3.13 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.13" + main_image: "quay.io/fedora/python-313" + + "3.12-minimal": + version: "3.12" + short_ver: "312" + minimal: true + base_img_version: "1" + # Python 3.12 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.12" + main_image: "quay.io/sclorg/python-312-minimal-c9s" + + "3.12": + version: "3.12" + short_ver: "312" + minimal: false + base_img_version: "1" + # Python 3.12 doesn't come as a module + module_stream: "" + pkg_prefix: "python3.12" + main_image: "registry.access.redhat.com/ubi9/python-312" + + "3.9": + version: "3.9" + short_ver: "39" + minimal: false + base_img_version: "1" + module_stream: "python39:3.9" + pkg_prefix: "python39" + main_image: "registry.access.redhat.com/ubi9/python-39" + + "3.9-minimal": + version: "3.9" + short_ver: "39" + minimal: true + base_img_version: "1" + module_stream: "python39:3.9" + pkg_prefix: "python39" + main_image: "quay.io/sclorg/python-39-minimal-c9s" + + "3.6": + version: "3.6" + short_ver: "36" + minimal: false + base_img_version: "1" + module_stream: "python36:3.6" + pkg_prefix: "python3" + main_image: "registry.access.redhat.com/ubi8/python-36" + +matrix: + include: + - distros: + - rhel-8-x86_64 + version: "3.6" + - distros: + - centos-stream-9-x86_64 + - rhel-9-x86_64 + version: "3.9" + - distros: + - centos-stream-9-x86_64 + version: "3.9-minimal" + - distros: + - centos-stream-10-x86_64 + - centos-stream-9-x86_64 + - rhel-9-x86_64 + - rhel-8-x86_64 + version: "3.12" + - distros: + - centos-stream-10-x86_64 + - centos-stream-9-x86_64 + - rhel-8-x86_64 + - rhel-9-x86_64 + - rhel-10-x86_64 + version: "3.12-minimal" + - distros: + - fedora-42-x86_64 + - centos-stream-10-x86_64 + version: "3.13" + - distros: + - fedora-42-x86_64 + - centos-stream-10-x86_64 + version: "3.13-minimal" + - distros: + - fedora-43-x86_64 + - centos-stream-9-x86_64 + - rhel-9-x86_64 + version: "3.14" + - distros: + - fedora-43-x86_64 + - centos-stream-10-x86_64 + - centos-stream-9-x86_64 + - rhel-9-x86_64 + - rhel-10-x86_64 + version: "3.14-minimal" diff --git a/src/Dockerfile-minimal.template b/src/Dockerfile-minimal.template new file mode 100644 index 00000000..26ef35a2 --- /dev/null +++ b/src/Dockerfile-minimal.template @@ -0,0 +1,109 @@ +{% import "src/common.tpl" as common with context %} +{% import "src/" + config.os.id + "/macros.tpl" as macros with context %} +FROM {{ spec.minimal_image }} + + +EXPOSE 8080 + +ENV PYTHON_VERSION={{ spec.version }} \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + CNB_STACK_ID=com.redhat.stacks.ubi{{ spec.el_version }}-python-{{ spec.short_ver }} \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ + PIP_NO_CACHE_DIR=off \ + # The following variables are usually available from parent s2i images \ + STI_SCRIPTS_PATH=/usr/libexec/s2i \ + APP_ROOT=/opt/app-root \ + HOME=/opt/app-root/src \ + PLATFORM="el{{ spec.el_version }}" + +# /opt/app-root/bin - the main venv +# /opt/app-root/src/bin - app-specific binaries +# /opt/app-root/src/.local/bin - tools like pipenv +ENV PATH=$APP_ROOT/bin:$HOME/bin:$HOME/.local/bin:$PATH + +# Ensure the virtual environment is active in interactive shells +ENV BASH_ENV=${APP_ROOT}/bin/activate \ + ENV=${APP_ROOT}/bin/activate \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" + +ENV SUMMARY="Minimal platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python {{ spec.version }}" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python{{ spec.short_ver }},python-{{ spec.short_ver }},rh-python{{ spec.short_ver }}" \ + com.redhat.component="python-{{ spec.short_ver }}-container" \ + name="{{ spec.org }}/python-{{ spec.short_ver }}-minimal{% if spec.org == "sclorg" %}-{{ spec.prod }}{% endif %}" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir={{ spec.version }}-minimal/test/setup-test-app/ ubi{{ spec.el_version }}/python-{{ spec.short_ver }}-minimal python-sample-app" \ + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi{{ spec.el_version }}-python-{{ spec.short_ver }}-minimal" \ + maintainer="SoftwareCollections.org " + +# Very minimal set of packages +# Python is obvious in the Python container :) +# glibc-langpack-en is needed to set locale to en_US and disable warning about it +# findutils - find command is needed for fix-permissions script +# nss_wrapper - used in generate_container_user script +RUN INSTALL_PKGS="{{ spec.pkg_prefix }} glibc-langpack-en findutils nss_wrapper-libs" && \ + {% if spec.enable_epel_for and spec.version in spec.enable_epel_for %} + microdnf -y install --setopt=install_weak_deps=0 epel-release && \ + {% endif %} + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY {{ spec.version }}-minimal/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY {{ spec.version }}-minimal/root/ / + +# Python 3.7+ 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 {{ spec.version }}-minimal/root/opt/wheels /opt/wheels + +# This command sets (and also creates if necessary) +# the home directory - it has to be done here so the latter +# fix-permissions fixes this directory as well. +WORKDIR ${HOME} + +# - 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 \ + python{{ spec.version }} -m venv ${APP_ROOT} && \ + # We have to upgrade pip to a newer version because \ + # 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 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 && \ + rpm-file-permissions + +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/src/Dockerfile.template b/src/Dockerfile.template new file mode 100644 index 00000000..5c246c6e --- /dev/null +++ b/src/Dockerfile.template @@ -0,0 +1,92 @@ +{% import "src/common.tpl" as common with context %} +{% import "src/" + config.os.id + "/macros.tpl" as macros with context %} +{% if config.os.id == 'rhel' and spec.base_img_version %} +{% set img_tag = spec.base_img_version %} +{% elif spec.img_tag %} +{% set img_tag = spec.img_tag %} +{% endif %} +# This image provides a Python {{ spec.version }} environment you can use to run your Python +# applications. +FROM {{ spec.s2i_base }}{% if img_tag %}:{{ img_tag }}{% endif %} + + +EXPOSE 8080 + +ENV PYTHON_VERSION={{ spec.version }} \ + PATH=$HOME/.local/bin/:$PATH \ + PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ +{% if spec.version in spec.get("ubi_versions", []) %} + CNB_STACK_ID=com.redhat.stacks.ubi{{ spec.el_version }}-python-{{ spec.short_ver }} \ + CNB_USER_ID=1001 \ + CNB_GROUP_ID=0 \ +{% endif %} + PIP_NO_CACHE_DIR=off + +{{ macros.env_metadata() -}} +ENV SUMMARY="Platform for building and running Python $PYTHON_VERSION applications" \ + 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. \ +Python's elegant syntax and dynamic typing, together with its interpreted nature, \ +make it an ideal language for scripting and rapid application development in many areas \ +on most platforms." + +LABEL summary="$SUMMARY" \ + description="$DESCRIPTION" \ + io.k8s.description="$DESCRIPTION" \ + io.k8s.display-name="Python {{ spec.version }}" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,python,python{{ spec.short_ver }},python-{{ spec.short_ver }},rh-python{{ spec.short_ver }}" \ +{{ macros.labels(spec) }} +{# Use extra_pkgs for specific Python version or the default or empty list #} +{% if spec.extra_pkgs and spec.version in spec.extra_pkgs %} + {% set extra_pkgs = spec.extra_pkgs[spec.version] %} +{% elif spec.extra_pkgs and "default" in spec.extra_pkgs %} + {% set extra_pkgs = spec.extra_pkgs["default"] %} +{% else %} + {% set extra_pkgs = [] %} +{% endif %} +RUN INSTALL_PKGS="{{ common.list_pkgs(extra_pkgs + spec.python_pkgs + spec.base_pkgs) -}} + {% if spec.preinstall_cmd %} +{{ common.preinstall_cmd(spec) -}} + {% endif %} + {% if spec.enable_epel_for and spec.version in spec.enable_epel_for %} + yum -y install --setopt=install_weak_deps=0 epel-release && \ + {% endif %} + {{ commands.pkginstaller.install([], {'docs': False}) }}{{ common.enablerepo(spec) }} $INSTALL_PKGS && \ + rpm -V $INSTALL_PKGS && \ + {% if spec.logos %} + # Remove {{ spec.logos }} (httpd dependency) to keep image size smaller. + rpm -e --nodeps {{ spec.logos }} && \ + {% endif %} + {{ commands.pkginstaller.cleancache() }} + +# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH. +COPY {{ spec.version }}/s2i/bin/ $STI_SCRIPTS_PATH + +# Copy extra files to the image. +COPY {{ spec.version }}/root/ / + +{% if spec.version != "3.6" %} +# Python 3.7+ 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 {{ spec.version }}/root/opt/wheels /opt/wheels +{% endif %} +# - 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. +{{ macros.venv_setup(spec) }} +USER 1001 + +# Set the default CMD to print the usage of the language image. +CMD $STI_SCRIPTS_PATH/usage diff --git a/src/README-minimal.md b/src/README-minimal.md new file mode 100644 index 00000000..0b4f3d19 --- /dev/null +++ b/src/README-minimal.md @@ -0,0 +1,126 @@ +Python {{ spec.version }} container image - minimal version +============================================ + +This container image is a special version of the [full Python {{ spec.version }} container image](https://github.com/sclorg/s2i-python-container/tree/master/{{ spec.version }}) +provided as a [S2I](https://github.com/openshift/source-to-image) base image for your Python {{ spec.version }} applications. + +Because the minimal and full images work similarly, we document here only the differences and limitations +of the minimal container image. For the documentation of common features see the [full container image docs](https://github.com/sclorg/s2i-python-container/tree/master/{{ spec.version }}). + +The Python {{ spec.version }} minimal container image is currently considered a tech-preview and only available on quay.io. +The image is built on top of the {% if spec.version == 3.8 %}[Red Hat Universal Base Image 8 Micro image](https://catalog.redhat.com/software/containers/ubi8-micro/601a84aadd19c7786c47c8ea) +and therefore uses the RPM packages from Red Hat Enterprise Linux, the same that are used by Python 3.9 supported container image and are part of the [UBI registry](https://www.redhat.com/en/blog/introducing-red-hat-universal-base-image){% else %}[official CentOS Stream base containers](quay.io/centos/centos){% endif %}. + +To pull the Python {{ spec.version }} minimal container image to build on, run + +``` +podman pull {{ spec.main_image }} +``` + +Description +----------- + +The full container image is a universal base image to build your containerized applications on top of. However, its universal nature +means that the resulting containers it produces consume a lot of disk space. This is caused mainly by the fact that the image contains +npm, compilers, header files and some other packages one might need to install and deploy their applications. + +Because size does matter for us and our customers, we have prepared this minimal container image with very limited subset +of installed packages. There are no compilers, no header files, no npm etc and the yum package manager is replaced with a minimalistic +reimplementation called microdnf, making the resulting container images much smaller. This creates some limitations +but we provide ways to workaround them. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Limitations +----------- + +1. There is only a very limited subset of packages installed. They are choosen carefully to satisfy most of the Python apps but your app might have some special needs. +1. There is no npm and nodejs. +1. There are no compilers and header files. Installation from Python wheels should still work but compilation from a source code is not supported out of the box. + +In the next chapter, we provide three possible workarounds for the mentioned limitations of the minimal container image. + +Possible solutions for the limitations +-------------------------------------- + +### Use the full container image + +It's easy at that. If you don't want to write your own Dockerfile and disk space is not a problem, use +the full universal container image and you should be fine. + +### Build your own container image on top of the minimal container image + +Let's say that your application depends on uwsgi. uwsgi cannot be installed from Python wheel and has to be +compiled from source which requires some additional packages to be installed - namely gcc for the compilation +itself and {{ spec.pkg_prefix }}-devel containing Python header files. + +To solve that problem, you can use all the pieces provided by the minimal container image and just add one more +step to install the missing dependencies: + +``` +FROM python-{{ spec.short_ver }}-minimal + +# 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 + +# Install packages necessary for compiling uwsgi from source +RUN microdnf install -y gcc {{ spec.pkg_prefix }}-devel +USER 1001 + +# Install the dependencies +RUN /usr/libexec/s2i/assemble + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +If you do it this way, your problem with the missing packages is solved. But there is also one disadvantage: the resulting +runtime image contains unnecessary compiler and Python header files. How to solve this? Uninstalling them at the end +of the Dockerfile is not really a solution but we have one. Keep reading. + +### Build on full image, run on minimal image + +Did you know that you can copy files from one image to another one during a build? That's the feature we are gonna use now. +We use the full container image with all compilers and other usefull packages installed to build our app and its dependencies +and we then move the result including the whole virtual environemnt to the minimal container image. + +This app needs mod_wsgi and to install (compile it from source) it, we'll need: httpd-devel for header files, gcc and redhat-rpm-config +as a compiler and configuratuion and finally {{ spec.pkg_prefix }}-devel containing Python header files. There is no need to install those packages +manually because the full container image already contains them. However, the application needs httpd as a runtime dependency +so we need to install it to the minimal container image as well. + +``` +# Part 1 - build + +FROM python-{{ spec.short_ver }} 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +# Part 2 - deploy + +FROM python-{{ spec.short_ver }}-minimal + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd package - runtime dependency of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run +``` + +This way, the resulting container image does contain only necessary dependencies and it's much lighter. diff --git a/src/README.md b/src/README.md new file mode 100644 index 00000000..77d071c7 --- /dev/null +++ b/src/README.md @@ -0,0 +1,388 @@ +Python {{ spec.version }} container image +========================= + +This container image includes Python {{ spec.version }} as a [S2I](https://github.com/openshift/source-to-image) base image for your Python {{ spec.version }} applications. +Users can choose between RHEL, CentOS Stream, and Fedora based builder images. +The RHEL images are available in the [Red Hat Container Catalog](https://catalog.redhat.com/software/containers/explore), +CentOS Stream images are available on [Quay.io](https://quay.io/organization/sclorg), +and the Fedora images are available on [Quay.io](https://quay.io/organization/fedora). +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 + +Description +----------- + +Python {{ spec.version }} available as container is a base platform for +building and running various Python {{ spec.version }} applications and frameworks. +Python is an easy to learn, powerful programming language. It has efficient high-level +data structures and a simple but effective approach to object-oriented programming. +Python's elegant syntax and dynamic typing, together with its interpreted nature, +make it an ideal language for scripting and rapid application development in many areas +on most platforms. + +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. + +See [the Red Hat Enterprise Linux Application Streams Life Cycle page](https://access.redhat.com/support/policy/updates/rhel-app-streams-life-cycle) for information about support for a particular stream. + +Usage in Openshift +------------------ + +For this, we will assume that you are using one of the supported images available via imagestream tags in Openshift, eg. `python:{{ spec.version }}-ubi9` +Building a simple [python-sample-app](https://github.com/sclorg/django-ex.git) application +in Openshift can be achieved with the following step: + + ``` + oc new-app python:{{ spec.version }}-ubi9~https://github.com/sclorg/django-ex.git + ``` + +**Accessing the application:** +``` +$ 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/4.14/openshift_images/create-images.html#images-create-s2i_create-images) +(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. + +To support the Source-to-Image framework, important scripts are included in the builder image: + +* 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. + +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. + +To use the Python image in a Dockerfile, follow these steps: +#### 1. Pull a base builder image to build on + +``` +podman pull {{ spec.main_image }} +``` + +#### 2. Pull and application code + +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 + +``` +git clone https://github.com/sclorg/django-ex.git app-src +``` + +#### 3. Prepare an application inside a container + +This step usually consists of at least these parts: + +* putting the application source into the container +* installing the dependencies +* setting the default command in the resulting image + +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. + +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. + +To use the Source-to-Image scripts and build an image using a Dockerfile, create a Dockerfile with this content: + +``` +FROM {{ spec.main_image }} + +# 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 +``` + +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: + +``` +FROM {{ spec.main_image }} + +# 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 +``` + +#### 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 +--------------------- + +To set these environment variables, you can place them as a key value pair into a `.s2i/environment` +file inside your source code repository. + +* **APP_SCRIPT** + + Used to run the application from a script file. + This should be a path to a script file (defaults to `app.sh` unless set to null) that will be + run to start the application. + +* **APP_FILE** + + Used to run the application from a Python script. + This should be a path to a Python file (defaults to `app.py` unless set to null) that will be + passed to the Python interpreter to start the application. + +* **APP_MODULE** + + Used to run the application with Gunicorn, as documented + [here](http://docs.gunicorn.org/en/latest/run.html#gunicorn). + This variable specifies a WSGI callable with the pattern + `MODULE_NAME:VARIABLE_NAME`, where `MODULE_NAME` is the full dotted path + of a module, and `VARIABLE_NAME` refers to a WSGI callable inside the + specified module. + Gunicorn will look for a WSGI callable named `application` if not specified. + + If `APP_MODULE` is not provided, the `run` script will look for a `wsgi.py` + file in your project and use it if it exists. + + If using `setup.py` for installing the application, the `MODULE_NAME` part + can be read from there. For an example, see + [setup-test-app](https://github.com/sclorg/s2i-python-container/tree/master/{{ spec.version }}/test/setup-test-app). + +* **APP_HOME** + + This variable can be used to specify a sub-directory in which the application to be run is contained. + The directory pointed to by this variable needs to contain `wsgi.py` (for Gunicorn) or `manage.py` (for Django). + + If `APP_HOME` is not provided, the `assemble` and `run` scripts will use the application's root + directory. + +* **APP_CONFIG** + + Path to a valid Python file with a + [Gunicorn configuration](http://docs.gunicorn.org/en/latest/configure.html#configuration-file) file. + +* **DISABLE_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_SETUP_PY_PROCESSING** / **DISABLE_PYPROJECT_TOML_PROCESSING** + + Set this to a non-empty value to skip processing of setup.{py,cfg} or pyproject.toml 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 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 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, capped + at 12. + + +Source repository layout +------------------------ + +You do not need to change anything in your existing Python project's repository. +However, if these files exist they will affect the behavior of the build process: + +* **requirements.txt** + + List of dependencies to be installed with `pip`. The format is documented + [here](https://pip.pypa.io/en/latest/user_guide.html#requirements-files). + + +* **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/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 container image produced by s2i-python executes your project in one of the +following ways, in precedence order: + +* **Gunicorn** + + The Gunicorn WSGI HTTP server is used to serve your application in the case that it + is installed. It can be installed by listing it either in the `requirements.txt` + file or in the `install_requires` section of the `setup.py` file. + + If a file named `wsgi.py` is present in your repository, it will be used as + the entry point to your application. This can be overridden with the + environment variable `APP_MODULE`. + This file is present in Django projects by default. + + If you have both Django and Gunicorn in your requirements, your Django project + will automatically be served using Gunicorn. + + 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 + application will be served using Django's development web server. However, this is not + recommended for production environments. + +* **Python script** + + This would be used where you provide a Python code file for running you + application. It will be used in the case where you specify a path to a + Python script via the `APP_FILE` environment variable, defaulting to a + file named `app.py` if it exists. The script is passed to a regular + Python interpreter to launch your application. + +* **Application script file** + + This is the most general way of executing your application. It will be + used in the case where you specify a path to an executable script file + via the `APP_SCRIPT` environment variable, defaulting to a file named + `app.sh` if it exists. The script is executed directly to launch your + application. + +Hot deploy +---------- + +If you are using Django, hot deploy will work out of the box. + +To enable hot deploy while using Gunicorn, make sure you have a Gunicorn +configuration file inside your repository with the +[`reload`](https://gunicorn-docs.readthedocs.org/en/latest/settings.html#reload) +option set to `true`. Make sure to specify your config via the `APP_CONFIG` +environment variable. + +To change your source code in running container, use podman's (or docker's) +[exec](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-exec.1.md) command: + +``` +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 RHEL8 is called `Dockerfile.rhel8`, for RHEL9 it's `Dockerfile.rhel9`, +for CentOS Stream 9 it's `Dockerfile.c9s`, for CentOS Stream 10 it's `Dockerfile.c10s`, +and the Fedora Dockerfile is called `Dockerfile.fedora`. diff --git a/src/centos-stream/macros.tpl b/src/centos-stream/macros.tpl new file mode 100644 index 00000000..39ae041c --- /dev/null +++ b/src/centos-stream/macros.tpl @@ -0,0 +1,43 @@ +{% macro env_metadata() %} +ENV NAME=python3 \ + ARCH=x86_64 + +{% endmacro %} + +{% macro _image_name(spec) %} +{{ spec.org }}/python-{{ spec.short_ver }}-{{ spec.prod -}} +{% endmacro %} + +{% macro labels(spec) %} + com.redhat.component="$NAME" \ + name="{{ _image_name(spec) }}" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir={{ + spec.version }}/test/setup-test-app/ $FGC/$NAME python-sample-app" \ + maintainer="SoftwareCollections.org " +{% endmacro %} + +{% macro venv_setup(spec) %} +RUN python{{ spec.version }} -m venv ${APP_ROOT} && \ +{% if spec.version != "3.6" %} +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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-* && \ +{% endif %} +rm -r /opt/wheels && \ +chown -R 1001:0 ${APP_ROOT} && \ +fix-permissions ${APP_ROOT} -P && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" +{% endmacro %} diff --git a/src/common.tpl b/src/common.tpl new file mode 100644 index 00000000..c7314d47 --- /dev/null +++ b/src/common.tpl @@ -0,0 +1,16 @@ +{% macro list_pkgs(pkgs) %} +{% for n in range(0, pkgs|length, 5) %} +{% if not loop.first %} {% endif %}{{ ' '.join(pkgs[n:n+5]) }}{% if loop.last %}" &&{% endif %} \ +{% endfor %} +{% endmacro %} + +{% macro enablerepo(spec) %} +{% if spec.enablerepo %} {{ spec.enablerepo }}{% endif %} +{% endmacro %} + +{% macro preinstall_cmd(spec) %} +{% for cmd in spec.preinstall_cmd %} + {{ cmd }} && \ +{% endfor %} +{% endmacro %} + diff --git a/src/fedora/macros.tpl b/src/fedora/macros.tpl new file mode 100644 index 00000000..3b61bd0d --- /dev/null +++ b/src/fedora/macros.tpl @@ -0,0 +1,39 @@ +{% macro env_metadata() %} +ENV NAME=python3 \ + ARCH=x86_64 + +{% endmacro %} + +{% macro labels(spec) %} + com.redhat.component="$NAME" \ + name="fedora/$NAME-{{ spec.short_ver }}" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir={{ + spec.version }}/test/setup-test-app/ quay.io/fedora/$NAME-{{ spec.short_ver }} python-sample-app" \ + maintainer="SoftwareCollections.org " +{% endmacro %} + +{% macro venv_setup(spec) %} +RUN python{{ spec.version }} -m venv ${APP_ROOT} && \ +{% if spec.version != "3.6" %} +# Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ +# We have to upgrade pip to a newer verison because \ +# 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-* && \ +{% endif %} +rm -r /opt/wheels && \ +chown -R 1001:0 ${APP_ROOT} && \ +fix-permissions ${APP_ROOT} -P && \ +# The following echo adds the unset command for the variables set below to the \ +# venv activation script. This prevents the virtual environment from being \ +# activated multiple times and also every time the prompt is rendered. \ +echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" +{% endmacro %} diff --git a/src/rhel/macros.tpl b/src/rhel/macros.tpl new file mode 100644 index 00000000..46331e87 --- /dev/null +++ b/src/rhel/macros.tpl @@ -0,0 +1,53 @@ +{% macro env_metadata() %}{% endmacro %} + +{% macro _component_name(spec) -%} +python-{{ spec.short_ver }}-container +{%- endmacro %} + +{% macro _image_name(spec) %} +{{ spec.org }}/python-{{ spec.short_ver }} +{%- endmacro %} + +{% macro labels(spec) %} + com.redhat.component="{{ _component_name(spec) }}" \ + name="{{ _image_name(spec) }}" \ + usage="s2i build https://github.com/sclorg/s2i-python-container.git --context-dir={{spec.version }}/test/setup-test-app/ {{ _image_name(spec) }} python-sample-app" \ + {% if spec.version in spec.get("ubi_versions", []) %} + com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" \ + io.buildpacks.stack.id="com.redhat.stacks.ubi{{ spec.el_version }}-python-{{ spec.short_ver }}" \ + {% endif %} + maintainer="SoftwareCollections.org " +{% endmacro %} + +{% macro venv_setup(spec) %} +RUN \ +{# Use different virtualenv command of venv module based on Python and platform version #} +{% if spec.version != "3.6" %} + python{{ spec.version }} -m venv ${APP_ROOT} && \ +{% else %} + virtualenv-$PYTHON_VERSION ${APP_ROOT} && \ +{% endif %} +{% if spec.version != "3.6" %} + # Python 3.7+ only code, Python <3.7 installs pip from PyPI in the assemble script. \ + # We have to upgrade pip to a newer verison because \ + # 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-* && \ +{% endif %} + rm -r /opt/wheels && \ + chown -R 1001:0 ${APP_ROOT} && \ + fix-permissions ${APP_ROOT} -P && \ + rpm-file-permissions && \ + # The following echo adds the unset command for the variables set below to the \ + # venv activation script. This prevents the virtual environment from being \ + # activated multiple times and also every time the prompt is rendered. \ + echo "unset BASH_ENV PROMPT_COMMAND ENV" >> ${APP_ROOT}/bin/activate +# Ensure the virtualenv is activated in interactive shells +ENV BASH_ENV="${APP_ROOT}/bin/activate" \ + ENV="${APP_ROOT}/bin/activate" \ + PROMPT_COMMAND=". ${APP_ROOT}/bin/activate" +{% endmacro %} diff --git a/src/root/opt/app-root/etc/generate_container_user b/src/root/opt/app-root/etc/generate_container_user new file mode 100644 index 00000000..a7fd74d6 --- /dev/null +++ b/src/root/opt/app-root/etc/generate_container_user @@ -0,0 +1,19 @@ +# Set current user in nss_wrapper +USER_ID=$(id -u) +GROUP_ID=$(id -g) + +if [ x"$USER_ID" != x"0" -a x"$USER_ID" != x"1001" ]; then + + NSS_WRAPPER_PASSWD=/opt/app-root/etc/passwd + NSS_WRAPPER_GROUP=/etc/group + + cat /etc/passwd | sed -e 's/^default:/builder:/' > $NSS_WRAPPER_PASSWD + + echo "default:x:${USER_ID}:${GROUP_ID}:Default Application User:${HOME}:/bin/bash" >> $NSS_WRAPPER_PASSWD + + export NSS_WRAPPER_PASSWD + export NSS_WRAPPER_GROUP + + LD_PRELOAD=libnss_wrapper.so + export LD_PRELOAD +fi diff --git a/src/root/opt/wheels/pip-24.2-py3-none-any.whl b/src/root/opt/wheels/pip-24.2-py3-none-any.whl new file mode 100755 index 00000000..ba32df83 Binary files /dev/null and b/src/root/opt/wheels/pip-24.2-py3-none-any.whl differ diff --git a/src/s2i/bin/assemble b/src/s2i/bin/assemble new file mode 100755 index 00000000..1ba7ba0e --- /dev/null +++ b/src/s2i/bin/assemble @@ -0,0 +1,155 @@ +#!/bin/bash + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_collectstatic() { + is_django_installed && [[ -z "$DISABLE_COLLECTSTATIC" ]] +} + +function virtualenv_bin() { + {% if spec.version != "3.6" %} + # New versions of Python (>3.6) should use venv module + # from stdlib instead of virtualenv package + python{{ spec.version }} -m venv $1 + {% else %} + if head "/etc/redhat-release" | grep -q "^CentOS Linux release 7" || \ + head "/etc/redhat-release" | grep -q "^Red Hat Enterprise Linux\( Server\)\? release 7" || \ + head "/etc/redhat-release" | grep -q "^Fedora release"; then + virtualenv $1 + else + virtualenv-${PYTHON_VERSION} $1 + fi + {% endif %} +} + +# 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/* "$HOME" + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P + +{% if spec.version == "3.6" %} +# Python <3.7 only code, Python 3.7+ uses bundled wheel from Fedora +# We have to first upgrade pip to at least 19.3 because +# pip < 19.3 does not support manylinux2014 wheels. Only manylinux2014 (and later) wheels +# support platforms like ppc64le, aarch64 or armv7 +echo "---> Upgrading pip to latest version supported for Python $PYTHON_VERSION ..." +if ! pip install -U "pip"; then + echo "WARNING: Installation of 'pip' failed, trying again from official PyPI with pip --isolated install" + pip install --isolated -U "pip" +fi +{% endif %} + +if [[ ! -z "$PIP_INDEX_URL" ]]; then + echo "---> Creating pip.ini config file" + echo "[global] +index-url = $PIP_INDEX_URL" >> /opt/app-root/src/pip.ini +fi + +if [[ ! -z "$UPGRADE_PIP_TO_LATEST" ]]; then + 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 [[ ! -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 || -f setup.cfg ) && -z "$DISABLE_SETUP_PY_PROCESSING" ]]; then + echo "---> Installing application (via setup.{py,cfg})..." + pip install . +fi + +if [[ -f pyproject.toml && -z "$DISABLE_PYPROJECT_TOML_PROCESSING" ]]; then + echo "---> Installing application (via pyproject.toml)..." + 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" + + # 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." + echo "'manage.py collectstatic' ignored." + exit + fi + + if ! python $manage_file collectstatic --dry-run --noinput &> /dev/null; then + echo "WARNING: could not run 'manage.py collectstatic'. To debug, run:" + echo " $ python $manage_file collectstatic --noinput" + echo "Ignore this warning if you're not serving static files with Django." + exit + fi + + python $manage_file collectstatic --noinput + ) +fi + +# set permissions for any installed artifacts +fix-permissions /opt/app-root -P diff --git a/src/s2i/bin/init-wrapper b/src/s2i/bin/init-wrapper new file mode 100644 index 00000000..26c8767d --- /dev/null +++ b/src/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/src/s2i/bin/run b/src/s2i/bin/run new file mode 100755 index 00000000..2e4ebca2 --- /dev/null +++ b/src/s2i/bin/run @@ -0,0 +1,161 @@ +#!/bin/bash +source /opt/app-root/etc/generate_container_user + +set -e + +function is_gunicorn_installed() { + hash gunicorn &>/dev/null +} + +function is_django_installed() { + python -c "import django" &>/dev/null +} + +function should_migrate() { + is_django_installed && [[ -z "$DISABLE_MIGRATE" ]] +} + +# Guess the number of workers according to the number of cores +function get_default_web_concurrency() { + {% if spec.minimal %} + # Using python and just python here because the script has + # platform-python in its shebang which we don't have in the minimal image + limit_vars=$(python /usr/bin/cgroup-limits) + {% else %} + limit_vars=$(cgroup-limits) + {% endif %} + local $limit_vars + if [ -z "${NUMBER_OF_CORES:-}" ]; then + echo 1 + return + fi + + local max=$((NUMBER_OF_CORES*2)) + # Require at least 43 MiB and additional 40 MiB for every worker + 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 +} + +function maybe_run_in_init_wrapper() { + if [[ -z "$ENABLE_INIT_WRAPPER" ]]; then + exec "$@" + else + exec $STI_SCRIPTS_PATH/init-wrapper "$@" + fi +} + +# Look for gunicorn>=20.1.0 to utilize gunicorn.conf.py +if is_gunicorn_installed && [[ -f "gunicorn.conf.py" ]]; then + ret=$(python -c 'import gunicorn +ver = gunicorn.version_info +print(0 if ver[0]>=21 or (ver[0] == 20 and ver[1] >= 1) else 1)') + grep -q "wsgi_app" gunicorn.conf.py && grep_result=0 || grep_result=1 + if [[ $ret -eq 0 ]] && [[ $grep_result -eq 0 ]]; then + echo "---> Using gunicorn.conf.py" + echo "---> Serving application with gunicorn ..." + exec gunicorn + fi +fi + +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 + +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 + +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 + echo "---> Migrating database ..." + python "$manage_file" migrate --noinput + else + echo "WARNING: seems that you're using Django, but we could not find a 'manage.py' file." + echo "Skipped 'python manage.py migrate'." + fi +fi + +# If 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 + 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)} + + # 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; we also pass them as + # arguments explicitly for compatibility with older Gunicorn versions. + 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 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:$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'." + fi +fi + +>&2 echo "ERROR: don't know how to run your application." +>&2 echo "Please set either APP_MODULE, APP_FILE or APP_SCRIPT environment variables, or create a file 'app.py' to launch your application." +exit 1 diff --git a/src/s2i/bin/usage b/src/s2i/bin/usage new file mode 100755 index 00000000..410977ac --- /dev/null +++ b/src/s2i/bin/usage @@ -0,0 +1,18 @@ +#!/bin/sh + +DISTRO=`cat /etc/*-release | grep ^ID= | grep -Po '".*?"' | tr -d '"'` +NAMESPACE=centos +[[ $DISTRO =~ rhel* ]] && NAMESPACE=rhscl + +cat < -- curl 127.0.0.1:8080 +EOF diff --git a/src/test/from-dockerfile/Dockerfile.tpl b/src/test/from-dockerfile/Dockerfile.tpl new file mode 100644 index 00000000..4249e1ee --- /dev/null +++ b/src/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/src/test/from-dockerfile/Dockerfile_no_s2i.tpl b/src/test/from-dockerfile/Dockerfile_no_s2i.tpl new file mode 100644 index 00000000..da2f43ff --- /dev/null +++ b/src/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/src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl b/src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl new file mode 100644 index 00000000..946157e5 --- /dev/null +++ b/src/test/from-dockerfile/mod_wsgi.Dockerfile.tpl @@ -0,0 +1,28 @@ +# The lowercase "from" is a dirty hack! TODO: Fix it! +# It's needed because ct_test_app_dockerfile changes the source image +# to the one currently in the testing and we need to avoid that here +# so we can prepare our own dockerfile with two different source images. +from #FULL_IMAGE_NAME# 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 /usr/bin/fix-permissions /tmp/src +USER 1001 + +# Install the application's dependencies from PyPI +RUN /usr/libexec/s2i/assemble + +from #IMAGE_NAME# + +# Copy app sources together with the whole virtual environment from the builder image +COPY --from=builder $APP_ROOT $APP_ROOT + +# Install httpd and echant packages - runtime dependencies of our application +USER 0 +RUN microdnf install -y httpd +USER 1001 + +# Set the default command for the resulting image +CMD /usr/libexec/s2i/run diff --git a/src/test/from-dockerfile/uwsgi.Dockerfile.tpl b/src/test/from-dockerfile/uwsgi.Dockerfile.tpl new file mode 100644 index 00000000..ed16117f --- /dev/null +++ b/src/test/from-dockerfile/uwsgi.Dockerfile.tpl @@ -0,0 +1,20 @@ +FROM #IMAGE_NAME# # Replaced by sed in ct_test_app_dockerfile + +ENV UPGRADE_PIP_TO_LATEST=1 + +# 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 +# Install packages necessary for compiling uwsgi from source +# pkgconfig(python-{{ spec.version }}) is provided by both python3-devel in c9s +# and {{ spec.pkg_prefix }}-devel in UBI8. +RUN microdnf install -y gcc "pkgconfig(python-{{ spec.version }})" which +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/src/test/pin-pipenv-version-test-app/.s2i/environment b/src/test/pin-pipenv-version-test-app/.s2i/environment new file mode 100644 index 00000000..d859ce70 --- /dev/null +++ b/src/test/pin-pipenv-version-test-app/.s2i/environment @@ -0,0 +1,8 @@ +ENABLE_PIPENV=1 +{% if spec.version in ["3.6", "3.8", "3.9"] %} +PIN_PIPENV_VERSION=2021.5.29 +{% elif spec.version in ["3.12", "3.13"] %} +PIN_PIPENV_VERSION=2023.11.14 +{% else %} +PIN_PIPENV_VERSION=2026.0.3 +{% endif %} diff --git a/src/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment b/src/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment new file mode 100644 index 00000000..e3932ee3 --- /dev/null +++ b/src/test/pipenv-and-micropipenv-should-fail-test-app/.s2i/environment @@ -0,0 +1,2 @@ +ENABLE_PIPENV=true +ENABLE_MICROPIPENV=true diff --git a/src/test/pipenv-test-app/3.12/Pipfile.lock b/src/test/pipenv-test-app/3.12/Pipfile.lock new file mode 100644 index 00000000..1d3f18e5 --- /dev/null +++ b/src/test/pipenv-test-app/3.12/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "08628d8adcf5d297b100b9dbb14807af951f225a705b902e39726cbbbaf91616" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1", + "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474" + ], + "markers": "python_version >= '3.6'", + "version": "==2023.11.17" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0", + "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033" + ], + "markers": "python_version >= '3.5'", + "version": "==21.2.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07", + "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.18" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + } + } +} diff --git a/src/test/pipenv-test-app/3.13/Pipfile.lock b/src/test/pipenv-test-app/3.13/Pipfile.lock new file mode 100644 index 00000000..de1ccaf4 --- /dev/null +++ b/src/test/pipenv-test-app/3.13/Pipfile.lock @@ -0,0 +1,211 @@ +{ + "_meta": { + "hash": { + "sha256": "1c042fcc7ce0692ac9ad83e0cc00f01a81991e88b45c33d6edc3e55ac79ae443" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.13" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" + ], + "markers": "python_version >= '3.6'", + "version": "==2024.8.30" + }, + "charset-normalizer": { + "hashes": [ + "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", + "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", + "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", + "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", + "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", + "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", + "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", + "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", + "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", + "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", + "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", + "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", + "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", + "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", + "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", + "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", + "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", + "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", + "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", + "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", + "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", + "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", + "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", + "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", + "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", + "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", + "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", + "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", + "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", + "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", + "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", + "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", + "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", + "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", + "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", + "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", + "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", + "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", + "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", + "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", + "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", + "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", + "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", + "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", + "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", + "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", + "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", + "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", + "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", + "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", + "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", + "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", + "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", + "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", + "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", + "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", + "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", + "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", + "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", + "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", + "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", + "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", + "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", + "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", + "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", + "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", + "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", + "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", + "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", + "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", + "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", + "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", + "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", + "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", + "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", + "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", + "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", + "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", + "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", + "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", + "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", + "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", + "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", + "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", + "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", + "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", + "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", + "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", + "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", + "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + ], + "markers": "python_full_version >= '3.7.0'", + "version": "==3.3.2" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "index": "pypi", + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "editable": true, + "path": "." + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "packaging": { + "hashes": [ + "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", + "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + ], + "markers": "python_version >= '3.8'", + "version": "==24.1" + }, + "pluggy": { + "hashes": [ + "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", + "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" + }, + "pytest": { + "hashes": [ + "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", + "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2" + ], + "index": "pypi", + "version": "==8.3.3" + } + } +} diff --git a/src/test/pipenv-test-app/3.14/Pipfile.lock b/src/test/pipenv-test-app/3.14/Pipfile.lock new file mode 100644 index 00000000..11c134ec --- /dev/null +++ b/src/test/pipenv-test-app/3.14/Pipfile.lock @@ -0,0 +1,242 @@ +{ + "_meta": { + "hash": { + "sha256": "97bbfa6552887882d1faa4fdebfc913bed2b4c622dd7d6ebf8d7a72155e29b4d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.14" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", + "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120" + ], + "markers": "python_version >= '3.7'", + "version": "==2026.1.4" + }, + "charset-normalizer": { + "hashes": [ + "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", + "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", + "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", + "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", + "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", + "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", + "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", + "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", + "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", + "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", + "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", + "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", + "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", + "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", + "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", + "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", + "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", + "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", + "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", + "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", + "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", + "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", + "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", + "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", + "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", + "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", + "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", + "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", + "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", + "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", + "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", + "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", + "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", + "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", + "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", + "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", + "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", + "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", + "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", + "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", + "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", + "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", + "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", + "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", + "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", + "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", + "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", + "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", + "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", + "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", + "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", + "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", + "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", + "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", + "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", + "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", + "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", + "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", + "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", + "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", + "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", + "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", + "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", + "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", + "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", + "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", + "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", + "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", + "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", + "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", + "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", + "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", + "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", + "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", + "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", + "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", + "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", + "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", + "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", + "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", + "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", + "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", + "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", + "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", + "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", + "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", + "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", + "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", + "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", + "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", + "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", + "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", + "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", + "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", + "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", + "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", + "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", + "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", + "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", + "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", + "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", + "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", + "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", + "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", + "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", + "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", + "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", + "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", + "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", + "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", + "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", + "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", + "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608" + ], + "markers": "python_version >= '3.7'", + "version": "==3.4.4" + }, + "gunicorn": { + "hashes": [ + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" + ], + "markers": "python_version >= '3.7'", + "version": "==23.0.0" + }, + "idna": { + "hashes": [ + "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", + "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902" + ], + "markers": "python_version >= '3.8'", + "version": "==3.11" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "requests": { + "hashes": [ + "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa", + "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf" + ], + "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", + "version": "==2.28.2" + }, + "setuptools": { + "hashes": [ + "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b", + "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990" + ], + "markers": "python_version >= '3.12'", + "version": "==67.7.2" + }, + "testapp": { + "version": "==0.1" + }, + "urllib3": { + "hashes": [ + "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.26.20" + } + }, + "develop": { + "iniconfig": { + "hashes": [ + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" + ], + "markers": "python_version >= '3.10'", + "version": "==2.3.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pluggy": { + "hashes": [ + "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", + "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" + }, + "pygments": { + "hashes": [ + "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", + "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b" + ], + "markers": "python_version >= '3.8'", + "version": "==2.19.2" + }, + "pytest": { + "hashes": [ + "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", + "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==9.0.2" + } + } +} diff --git a/src/test/pipenv-test-app/3.6/Pipfile.lock b/src/test/pipenv-test-app/3.6/Pipfile.lock new file mode 100644 index 00000000..9102b17b --- /dev/null +++ b/src/test/pipenv-test-app/3.6/Pipfile.lock @@ -0,0 +1,163 @@ +{ + "_meta": { + "hash": { + "sha256": "3cc443e101d93694814e3e48b272fafa4dc1efb36de327254b32c6683afc5b47" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" + ], + "version": "==2020.12.5" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "e1839a8": { + "editable": true, + "path": "." + }, + "gunicorn": { + "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", + "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "markers": "python_version >= '3.5'", + "version": "==20.1.0" + }, + "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:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:8c501196e49fb9df5df43833bdb1e4328f64847763ec8a50703148b73784d581", + "sha256:d7eb1dea6d6a6086f8be21784cc9e3bcfa55872b52309bc5fad53a8ea444465d" + ], + "markers": "python_version < '3.8' and python_version < '3.8'", + "version": "==4.0.1" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "packaging": { + "hashes": [ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.9" + }, + "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:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.4.7" + }, + "pytest": { + "hashes": [ + "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634", + "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc" + ], + "index": "pypi", + "version": "==6.2.3" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'", + "version": "==0.10.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], + "markers": "python_version < '3.8'", + "version": "==3.10.0.0" + }, + "zipp": { + "hashes": [ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], + "markers": "python_version >= '3.6'", + "version": "==3.4.1" + } + } +} diff --git a/src/test/pipenv-test-app/3.9/Pipfile.lock b/src/test/pipenv-test-app/3.9/Pipfile.lock new file mode 100644 index 00000000..25741732 --- /dev/null +++ b/src/test/pipenv-test-app/3.9/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/src/test/pipenv-test-app/Pipfile b/src/test/pipenv-test-app/Pipfile new file mode 100644 index 00000000..d6383329 --- /dev/null +++ b/src/test/pipenv-test-app/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +"e1839a8" = {path = ".", editable = true} +{% if spec.version in ["3.6", "3.8", "3.9"] %} +requests = "==2.20.0" +{% else %} +requests = "==2.28.2" +setuptools = {version = "==67.7.2", markers="python_version >= '3.12'"} +{% endif %} + +[dev-packages] +pytest = ">=2.8.0" + +[requires] +python_version = "{{ spec.version }}" diff --git a/src/test/pipenv-test-app/Pipfile.lock b/src/test/pipenv-test-app/Pipfile.lock new file mode 100644 index 00000000..196d59c6 --- /dev/null +++ b/src/test/pipenv-test-app/Pipfile.lock @@ -0,0 +1,13 @@ +{# + This file has to be manually generated via `pipenv lock` for every + new Python version. Just take Pipfile, generate Pipfile.lock and copy + it to src/test/pipenv-test-app//Pipfile.lock. + It's then included here ↓ when Pipfile.lock is generated by distgen. + + Steps: + 1. Go to examples/pipenv-test-app + 2. Update Pipfile - change Python version to the new one + 3. Run `pipenv lock` + 4. Copy Pipfile.lock to src/test/pipenv-test-app//Pipfile.lock +#} +{% include "src/test/pipenv-test-app/" + spec.version + "/Pipfile.lock" %} diff --git a/3.4/test/django-test-app/project/__init__.py b/test/__init__.py similarity index 100% rename from 3.4/test/django-test-app/project/__init__.py rename to test/__init__.py diff --git a/test/check_imagestreams.py b/test/check_imagestreams.py new file mode 120000 index 00000000..48e6cad0 --- /dev/null +++ b/test/check_imagestreams.py @@ -0,0 +1 @@ +../common/check_imagestreams.py \ No newline at end of file diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..8af74506 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,120 @@ +import os +import sys +from pathlib import Path +from collections import namedtuple + +from container_ci_suite.utils import check_variables +from pytest import skip + +if not check_variables(): + sys.exit(1) + +TAGS = { + "rhel8": "-ubi8", + "rhel9": "-ubi9", + "rhel10": "-ubi10", +} + + +Vars = namedtuple( + "Vars", + [ + "OS", + "VERSION", + "IMAGE_NAME", + "IS_MINIMAL", + "VERSION_NO_MINIMAL", + "SHORT_VERSION", + "TAG", + "TEST_DIR", + "WEB_APPS", + "SHOULD_FAIL_WEB_APPS", + "UNSTABLE_WEB_APP", + "BRANCH_TO_TEST", + "DEPLOYED_PSQL_IMAGE", + "IMAGE_TAG", + "PSQL_VERSION", + ], +) +VERSION = os.getenv("VERSION") +OS = os.getenv("TARGET").lower() +TEST_DIR = Path(__file__).parent.absolute() +BRANCH_TO_TEST = "4.2.x" +DEPLOYED_PSQL_IMAGE = "quay.io/sclorg/postgresql-12-c8s" +IMAGE_TAG = "postgresql:12" +PSQL_VERSION = "12" + +if VERSION in ("3.6"): + DEPLOYED_PSQL_IMAGE = "quay.io/sclorg/postgresql-10-c8s:c8s" + IMAGE_TAG = "postgresql:10" + PSQL_VERSION = "10" + BRANCH_TO_TEST = "2.2.x" + +COMMON_WEB_APPS = [ + TEST_DIR / f"{x}-test-app" + for x in [ + "gunicorn-config-different-port", + "gunicorn-different-port", + "django-different-port", + "standalone", + "setup", + "setup-requirements", + "django", + "numpy", + "app-home", + "locale", + "app-module", + "pyuwsgi-pipenv", + "micropipenv", + "standalone-custom-pypi-index", + "gunicorn-python-configfile-different-port", + ] +] +FULL_WEB_APPS = [ + TEST_DIR / f"{x}-test-app" + for x in [ + "setup-cfg", + "npm-virtualenv-uwsgi", + "mod-wsgi", + "pin-pipenv-version", + "micropipenv-requirements", + "poetry-src-layout", + ] +] +SHOULD_FAIL_WEB_APPS = [ + TEST_DIR / "pipenv-and-micropipenv-should-fail-test-app", +] +UNSTABLE_WEB_APP = [ + "pipenv-test-app", +] +if "minimal" in VERSION: + WEB_APPS = COMMON_WEB_APPS +else: + WEB_APPS = COMMON_WEB_APPS + FULL_WEB_APPS + +VARS = Vars( + OS=OS, + VERSION=VERSION, + IMAGE_NAME=os.getenv("IMAGE_NAME"), + IS_MINIMAL="minimal" in VERSION, + VERSION_NO_MINIMAL=VERSION.replace("-minimal", ""), + SHORT_VERSION=VERSION.replace("-minimal", "").replace(".", ""), + TAG=TAGS.get(OS), + TEST_DIR=TEST_DIR, + WEB_APPS=WEB_APPS, + SHOULD_FAIL_WEB_APPS=SHOULD_FAIL_WEB_APPS, + UNSTABLE_WEB_APP=UNSTABLE_WEB_APP, + BRANCH_TO_TEST=BRANCH_TO_TEST, + DEPLOYED_PSQL_IMAGE=DEPLOYED_PSQL_IMAGE, + IMAGE_TAG=IMAGE_TAG, + PSQL_VERSION=PSQL_VERSION, +) + + +def skip_helm_charts_tests(): + if VARS.VERSION in ("3.9-minimal") or ( + VARS.VERSION == "3.12-minimal" and VARS.OS == "rhel8" + ): + skip(f"Skipping Helm Charts tests for {VARS.VERSION} on {VARS.OS}.") + if VARS.VERSION in ("3.14", "3.14-minimal"): + skip(f"Skipping Helm Charts tests for {VARS.VERSION} on {VARS.OS}. The image is not GA available yet.") diff --git a/test/imagestreams b/test/imagestreams new file mode 120000 index 00000000..7af977a6 --- /dev/null +++ b/test/imagestreams @@ -0,0 +1 @@ +../imagestreams/ \ No newline at end of file diff --git a/test/run b/test/run new file mode 100755 index 00000000..2020f22e --- /dev/null +++ b/test/run @@ -0,0 +1,324 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies that S2I image. +# The main focus here is to excersise the S2I scripts. +# +# 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 COMMON_WEB_APPS=({gunicorn-config-different-port,gunicorn-different-port,django-different-port,standalone,setup,setup-requirements,django,numpy,app-home,locale,pipenv,pipenv-and-micropipenv-should-fail,app-module,pyuwsgi-pipenv,micropipenv,standalone-custom-pypi-index,gunicorn-python-configfile-different-port}-test-app) +declare -a FULL_WEB_APPS=({setup-cfg,npm-virtualenv-uwsgi,mod-wsgi,pin-pipenv-version,micropipenv-requirements,poetry-src-layout}-test-app) +declare -a MINIMAL_WEB_APPS=() +{% if spec.minimal %} +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${MINIMAL_WEB_APPS[@]}) +{% else %} +declare -a WEB_APPS=(${COMMON_WEB_APPS[@]} ${FULL_WEB_APPS[@]}) +{% endif %} + +# Some tests, like the one using the latest pipenv, might be unstable +# because new upstream releases tend to break our tests sometimes. +# If a test is in UNSTABLE_TESTS and IGNORE_UNSTABLE_TESTS env +# variable is defined, a result of the test has no impact on +# the overall result of the test suite. +# +# Reasons for specific tests to be marked as unstable: +# pipenv-test-app: +# - this testcase installs pipenv from the internet. +# Problem is that the upstream releases are from time-to-time broken +# which breaks this test. We generally want to know about it +# in upstream, but ignore this in downstream. +declare -a UNSTABLE_TESTS=(pipenv-test-app) + +# TODO: Make command compatible for Mac users +test_dir="$(readlink -f $(dirname "${BASH_SOURCE[0]}"))" +image_dir=$(readlink -f ${test_dir}/..) + +TEST_LIST="\ +test_s2i_usage +test_docker_run_usage +test_application +test_application_with_user +test_application_enable_init_wrapper +" + +TEST_VAR_DOCKER="\ +test_from_dockerfile{% if spec.minimal %}_minimal{% endif %} + +" + +if [[ -z $VERSION ]]; then + echo "ERROR: The VERSION variable must be set." + ct_check_testcase_result 1 + exit 1 +fi + +IMAGE_NAME=${IMAGE_NAME:-ubi9/python-${VERSION//./}} + +. test/test-lib.sh + +info() { + echo -e "\n\e[1m[INFO] $@\e[0m\n" +} + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + +{% raw %} +container_ip() { + docker inspect --format="{{ .NetworkSettings.IPAddress }}" $(cat $cid_file) +} +{% endraw %} + +run_s2i_build() { + info "Building the ${1} application image ..." + ct_s2i_build_as_df file://${test_dir}/${1} ${IMAGE_NAME} ${IMAGE_NAME}-testapp ${s2i_args} +} + +prepare() { + if ! image_exists ${IMAGE_NAME}; then + echo "ERROR: The image ${IMAGE_NAME} must exist before this script is executed." + return 1 + fi + # TODO: S2I build require the application is a valid 'GIT' repository, we + # should remove this restriction in the future when a file:// is used. + info "Preparing to test ${1} ..." + pushd ${test_dir}/${1} >/dev/null + git init + git config user.email "build@localhost" && git config user.name "builder" + git add -A && git commit -m "Sample commit" + popd >/dev/null +} + +run_test_application() { + docker run --user=100001 ${CONTAINER_ARGS} --rm --cidfile=${cid_file} ${IMAGE_NAME}-testapp +} + +cleanup() { + info "Cleaning up the test application image" + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi -f ${IMAGE_NAME}-testapp + fi + rm -rf ${test_dir}/${1}/.git +} +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + info "Waiting for application container to start $CONTAINER_ARGS ..." + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && [ -s $cid_file ] && return 0 + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return 1 +} + +test_s2i_usage() { + info "Testing 's2i usage' ..." + ct_s2i_usage ${IMAGE_NAME} ${s2i_args} 1>/dev/null +} + +test_docker_run_usage() { + info "Testing 'docker run' usage ..." + docker run --rm ${IMAGE_NAME} &>/dev/null +} + +test_scl_usage() { + local run_cmd="$1" + local expected="$2" + local cid_file="$3" + + info "Testing command availability in the image" + out=$(docker run --rm ${IMAGE_NAME} /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[/bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/bash -c "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/bash -c "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi + out=$(docker exec $(cat ${cid_file}) /bin/sh -ic "${run_cmd}" 2>&1) + if ! echo "${out}" | grep -q "${expected}"; then + echo "ERROR[exec /bin/sh -ic "${run_cmd}"] Expected '${expected}', got '${out}'" + return 1 + fi +} + +test_connection() { + info "Testing the HTTP connection (http://$(container_ip):${test_port}) ${CONTAINER_ARGS} ..." + local max_attempts=30 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + response_code=$(curl -s -w %{http_code} -o /dev/null http://$(container_ip):${test_port}/) + status=$? + if [ $status -eq 0 ]; then + if [ $response_code -eq 200 ]; then + result=0 + fi + break + fi + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done + return $result +} + +test_application() { + local cid_file="$CID_FILE_DIR"/"$(mktemp -u -p . --suffix .cid)" + # Verify that the HTTP connection can be established to test application container + run_test_application & + + # Wait for the container to write it's CID file + wait_for_cid + # Some test apps have tests in their startup code so we have to check + # that the container starts at all + ct_check_testcase_result $? + + # Instead of relying on VERSION variable coming from Makefile + # set the expected string based on the PYTHON_VERSION defined + # inside the running container. + python_version=$(docker run --rm $IMAGE_NAME /bin/bash -c "echo \$PYTHON_VERSION" 2>&1) + + test_scl_usage "python --version" "Python $python_version." "${cid_file}" + ct_check_testcase_result $? + {% if not spec.minimal %} + test_scl_usage "node --version" "^v[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + test_scl_usage "npm --version" "^[0-9]*\.[0-9]*\.[0-9]*" "${cid_file}" + ct_check_testcase_result $? + {% endif %} + test_connection + ct_check_testcase_result $? + container_exists && docker stop $(cat "$cid_file") +} + +test_from_dockerfile(){ + info "Test from Dockerfile" + {% if spec.version == "3.6" %} + django_example_repo_url="https://github.com/sclorg/django-ex.git@2.2.x" + {% else %} + # Django 4.2 supports Python 3.9+ + django_example_repo_url="https://github.com/sclorg/django-ex.git@4.2.x" + {% endif %} + + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile.tpl $django_example_repo_url 'Welcome to your Django application on OpenShift' app-src + ct_check_testcase_result $? + + 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 + ct_check_testcase_result $? +} + +test_from_dockerfile_minimal(){ + info "Test from Dockerfile" + + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + + # uwsgi in uwsgi-test-app + ct_test_app_dockerfile $test_dir/from-dockerfile/uwsgi.Dockerfile.tpl $test_dir/uwsgi-test-app 'Hello World from uWSGI hosted WSGI application!' app-src + ct_check_testcase_result $? + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + FULL_IMAGE_NAME=${IMAGE_NAME/-minimal/} + + if ct_pull_image "$FULL_IMAGE_NAME"; then + # mod_wsgi in micropipenv-requirements-test-app + sed "s@#IMAGE_NAME#@${IMAGE_NAME}@;s@#FULL_IMAGE_NAME#@${FULL_IMAGE_NAME}@" $test_dir/from-dockerfile/mod_wsgi.Dockerfile.tpl > $test_dir/from-dockerfile/Dockerfile + ct_test_app_dockerfile $test_dir/from-dockerfile/Dockerfile $test_dir/micropipenv-requirements-test-app 'Hello World from mod_wsgi hosted WSGI application!' app-src + ct_check_testcase_result $? + else + echo "[SKIP] Multistage build from Dockerfile - $FULL_IMAGE_NAME does not exists." + fi + +} + +test_application_with_user() { + # test application with random user + 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 +} + +# Positive test & non-zero exit status = ERROR. +# Negative test & zero exit status = ERROR. +# Tests with '-should-fail-' in their name should fail during a build, +# expecting non-zero exit status. +evaluate_build_result() { + local _result="$1" + local _app="$2" + local _type="positive" + local _test_msg="[PASSED]" + local _ret_code=0 + + if [[ "$_app" == *"-should-fail-"* ]]; then + _type="negative" + fi + + if [[ "$_type" == "positive" && "$_result" != "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED:0 GOT:${_result}" + _ret_code=$_result + elif [[ "$_type" == "negative" && "$_result" == "0" ]]; then + info "TEST FAILED (${_type}), EXPECTED: non-zero GOT:${_result}" + _ret_code=1 + fi + if [ $_ret_code != 0 ]; then + cleanup + TESTSUITE_RESULT=1 + _test_msg="[FAILED]" + fi + ct_update_test_result "$_test_msg" "$_app" run_s2i_build + + if [[ "$_type" == "negative" && "$_result" != "0" ]]; then + _ret_code=127 # even though this is success, the app is still not built + fi + return $_ret_code +} + +ct_init + +# 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} + if [ $? -ne 0 ]; then + ct_update_test_result "[FAILED]" "${app}" "preparation" + TESTSUITE_RESULT=1 + continue + fi + run_s2i_build ${app} + evaluate_build_result $? "$app" || continue + + TEST_SET=${TESTS:-$TEST_LIST} ct_run_tests_from_testset "${app}" + + cleanup ${app} +done + +TEST_SET=${TESTS:-$TEST_VAR_DOCKER} ct_run_tests_from_testset "var-docker" diff --git a/test/run-openshift-pytest b/test/run-openshift-pytest new file mode 100755 index 00000000..3687c0e5 --- /dev/null +++ b/test/run-openshift-pytest @@ -0,0 +1,11 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel9) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +cd "${THISDIR}" && python3.12 -m pytest -s -rA --showlocals -vv test_ocp_*.py diff --git a/test/run-pytest b/test/run-pytest new file mode 100755 index 00000000..9810fe89 --- /dev/null +++ b/test/run-pytest @@ -0,0 +1,16 @@ +#!/bin/bash +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# SINGLE_VERSION specifies the major version of the MariaDB in format of X.Y +# OS specifies RHEL version (e.g. OS=rhel8) +# + +THISDIR=$(dirname ${BASH_SOURCE[0]}) + +if python3 -c 'import sys; sys.exit(0 if sys.version_info < (3,13) else 1)'; then + PYTHON_VERSION="3.12" +else + PYTHON_VERSION="3" +fi +cd "${THISDIR}" && "python${PYTHON_VERSION}" -m pytest -s -rA --showlocals -vv test_container_*.py diff --git a/test/show_all_imagestreams.py b/test/show_all_imagestreams.py new file mode 120000 index 00000000..ba8fe973 --- /dev/null +++ b/test/show_all_imagestreams.py @@ -0,0 +1 @@ +../common/show_all_imagestreams.py \ No newline at end of file diff --git a/test/test-lib.sh b/test/test-lib.sh new file mode 120000 index 00000000..6ddf876e --- /dev/null +++ b/test/test-lib.sh @@ -0,0 +1 @@ +../common/test-lib.sh \ No newline at end of file diff --git a/test/test_container_application.py b/test/test_container_application.py new file mode 100644 index 00000000..f6b85994 --- /dev/null +++ b/test/test_container_application.py @@ -0,0 +1,118 @@ +import pytest + +from container_ci_suite.container_lib import ContainerImage, ContainerTestLib +from container_ci_suite.engines.podman_wrapper import PodmanCLIWrapper + +from conftest import VARS + + +class TestPythonApplication: + @pytest.fixture( + scope="class", params=VARS.WEB_APPS, ids=[app.name for app in VARS.WEB_APPS] + ) + def build_s2i_app(self, request): + container_lib = ContainerTestLib(VARS.IMAGE_NAME) + app_name = request.param.name + s2i_app = container_lib.build_as_df( + app_path=request.param, + s2i_args="--pull-policy=never", + src_image=VARS.IMAGE_NAME, + dst_image=f"{VARS.IMAGE_NAME}-{app_name}", + ) + yield s2i_app + s2i_app.cleanup() + + @pytest.mark.parametrize( + "container_args", + [ + "--user 100001", + "--user 12345", + "-e ENABLE_INIT_WRAPPER=true", + ], + ) + def test_application(self, build_s2i_app, container_args): + """ + Test if container works + Python version is properly set + Application response on specific port and specific URL. + + """ + cid_file_name = build_s2i_app.app_name + assert build_s2i_app.create_container( + cid_file_name=cid_file_name, + container_args=container_args, + ) + assert ContainerImage.wait_for_cid(cid_file_name=cid_file_name) + python_version = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="echo $PYTHON_VERSION" + ).strip() + python_version_output = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="python --version" + ) + assert f"Python {python_version}." in python_version_output + cip = build_s2i_app.get_cip(cid_file_name=cid_file_name) + assert cip + port = 8080 + if "different-port" in build_s2i_app.app_name: + port = 8085 + assert build_s2i_app.test_response(url=f"http://{cip}", port=port) + + +class TestPythonShouldFailApplication: + @pytest.mark.parametrize( + "should_fail_app", + VARS.SHOULD_FAIL_WEB_APPS, + ) + def test_application_should_fail(self, should_fail_app): + """ + Test if build fails for should fail apps + + """ + container_lib = ContainerTestLib(VARS.IMAGE_NAME) + app_name = should_fail_app.name + s2i_app_should_fail = container_lib.build_as_df( + app_path=should_fail_app, + s2i_args="--pull-policy=never", + src_image=VARS.IMAGE_NAME, + dst_image=f"{VARS.IMAGE_NAME}-{app_name}", + ) + assert not s2i_app_should_fail + + +class TestPythonUnstableApplication: + @pytest.mark.parametrize( + "unstable_app", + VARS.UNSTABLE_WEB_APP, + ) + def test_unstable_test(self, unstable_app): + """ + Test if unstable test works + """ + container_lib = ContainerTestLib(VARS.IMAGE_NAME) + s2i_app_unstable = container_lib.build_as_df( + app_path=VARS.TEST_DIR / f"{unstable_app}-test-app", + s2i_args="--pull-policy=never", + src_image=VARS.IMAGE_NAME, + dst_image=f"{VARS.IMAGE_NAME}-{unstable_app}", + ) + if not s2i_app_unstable: + pytest.skip(f"Build failed for unstable app {unstable_app}") + cid_file_name = s2i_app_unstable.app_name + assert s2i_app_unstable.create_container( + cid_file_name=cid_file_name, + container_args=s2i_app_unstable, + ) + python_version = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="echo $PYTHON_VERSION" + ).strip() + assert ContainerImage.wait_for_cid(cid_file_name=cid_file_name) + python_version_output = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="python --version" + ) + assert f"Python {python_version}." in python_version_output + cip = s2i_app_unstable.get_cip(cid_file_name=cid_file_name) + assert cip + port = 8080 + if "different-port" in s2i_app_unstable.app_name: + port = 8085 + assert s2i_app_unstable.test_response(url=f"http://{cip}", port=port) diff --git a/test/test_container_basics.py b/test/test_container_basics.py new file mode 100644 index 00000000..41d99b7f --- /dev/null +++ b/test/test_container_basics.py @@ -0,0 +1,150 @@ +import re + +import pytest + +from container_ci_suite.container_lib import ContainerTestLib +from container_ci_suite.engines.podman_wrapper import PodmanCLIWrapper +from container_ci_suite.utils import ContainerTestLibUtils + +from conftest import VARS + + +class TestS2IPythonContainer: + """ + Test if container works properly + """ + + def setup_method(self): + """ + Setup the test environment. + """ + self.app = ContainerTestLib() + + def teardown_method(self): + """ + Cleanup the test environment. + """ + self.app.cleanup() + + def test_run_s2i_usage(self): + """ + Test if s2i usage works + """ + assert self.app.s2i_usage() + + def test_docker_run_usage(self): + """ + Test if container is runnable + """ + assert ( + PodmanCLIWrapper.call_podman_command( + cmd=f"run --rm {VARS.IMAGE_NAME} &>/dev/null", return_output=False + ) + == 0 + ) + + def test_scl_usage(self): + """ + Test if python --version returns proper output + """ + python_version = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="echo $PYTHON_VERSION" + ).strip() + python_version_output = PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="python --version" + ) + assert f"Python {python_version}." in python_version_output + if not VARS.IS_MINIMAL: + assert ( + re.search( + r"v[0-9]*\.[0-9]*\.[0-9]*", + PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="node --version" + ), + ) + is not None + ) + assert ( + re.search( + r"^[0-9]*\.[0-9]*\.[0-9]*", + PodmanCLIWrapper.podman_run_command_and_remove( + cid_file_name=VARS.IMAGE_NAME, cmd="npm --version" + ), + ) + is not None + ) + + @pytest.mark.parametrize("dockerfile", ["Dockerfile.tpl", "Dockerfile_no_s2i.tpl"]) + def test_dockerfiles(self, dockerfile): + if "minimal" in VARS.VERSION: + pytest.skip("Skipping tests 'test_dockerfiles' for minimal versions.") + assert self.app.build_test_container( + dockerfile=VARS.TEST_DIR / "from-dockerfile" / dockerfile, + app_url=f"https://github.com/sclorg/django-ex.git@{VARS.BRANCH_TO_TEST}", + app_dir="app-src", + ) + assert self.app.test_app_dockerfile() + cip = self.app.get_cip() + assert cip + assert self.app.test_response( + url=cip, expected_output="Welcome to your Django application on OpenShift" + ) + + def test_minimal_dockerfiles(self): + if "minimal" not in VARS.VERSION: + pytest.skip( + "Skipping tests 'test_minimal_dockerfiles' for non-minimal versions." + ) + # The following tests are for multi-stage builds. These technically also work on full images, but there is no reason to do multi-stage builds with full images. + assert self.app.build_test_container( + dockerfile=VARS.TEST_DIR / "from-dockerfile/uwsgi.Dockerfile.tpl", + app_url=VARS.TEST_DIR / "uwsgi-test-app", + app_dir="app-src", + app_image_name="uwsgi-test-app", + ) + assert self.app.test_app_dockerfile() + cip = self.app.get_cip(cid_file_name="uwsgi-test-app") + assert cip + assert self.app.test_response( + url=cip, + expected_output="Hello World from uWSGI hosted WSGI application!", + ) + + # So far, for all the minimal images, the name of the full container image counterpart + # is the same just without -minimal infix. + # sclorg/python-39-minimal-c9s / sclorg/python-39-c9s + # ubi8/python-39-minimal / ubi8/python-39 + full_image_name = VARS.IMAGE_NAME.replace("-minimal", "") + is_pulled = PodmanCLIWrapper.podman_pull_image( + image_name=full_image_name, loops=1 + ) + print(f"Is image {full_image_name} pulled? {is_pulled}") + if is_pulled: + dockerfile_image_name = ContainerTestLibUtils.update_dockerfile( + dockerfile=VARS.TEST_DIR / "from-dockerfile/mod_wsgi.Dockerfile.tpl", + original_string="#IMAGE_NAME#", + string_to_replace=VARS.IMAGE_NAME, + ) + dockerfile = ContainerTestLibUtils.update_dockerfile( + dockerfile=dockerfile_image_name, + original_string="#FULL_IMAGE_NAME#", + string_to_replace=full_image_name, + ) + assert self.app.build_test_container( + dockerfile=dockerfile, + app_url=VARS.TEST_DIR / "micropipenv-requirements-test-app", + app_dir="app-src", + app_image_name="micropipenv-requirements-test-app", + ) + assert self.app.test_app_dockerfile() + cip = self.app.get_cip(cid_file_name="micropipenv-requirements-test-app") + assert cip + assert self.app.test_response( + url=cip, + expected_output="Hello World from mod_wsgi hosted WSGI application!", + debug=True, + ) + else: + pytest.skip( + f"The full container image for the minimal one {full_image_name} does not exist, skipping the rest of the tests." + ) diff --git a/test/test_ocp_deploy_templates.py b/test/test_ocp_deploy_templates.py new file mode 100644 index 00000000..535598cf --- /dev/null +++ b/test/test_ocp_deploy_templates.py @@ -0,0 +1,46 @@ +import pytest + +from container_ci_suite.openshift import OpenShiftAPI + +from conftest import VARS + + +class TestDeployTemplate: + def setup_method(self): + self.oc_api = OpenShiftAPI( + pod_name_prefix=f"python-{VARS.SHORT_VERSION}-test", + version=VARS.VERSION_NO_MINIMAL, + shared_cluster=True, + ) + + def teardown_method(self): + self.oc_api.delete_project() + + @pytest.mark.parametrize( + "template", ["django-postgresql.json", "django-postgresql-persistent.json"] + ) + def test_python_template_inside_cluster(self, template): + assert self.oc_api.upload_image(VARS.DEPLOYED_PSQL_IMAGE, VARS.IMAGE_TAG) + service_name = f"python-{VARS.SHORT_VERSION}-test" + template_url = self.oc_api.get_raw_url_for_json( + container="django-ex", + dir="openshift/templates", + filename=template, + branch=VARS.BRANCH_TO_TEST, + ) + assert self.oc_api.deploy_template_with_image( + image_name=VARS.IMAGE_NAME, + template=template_url, + name_in_template="python", + openshift_args=[ + f"SOURCE_REPOSITORY_REF={VARS.BRANCH_TO_TEST}", + f"PYTHON_VERSION={VARS.VERSION_NO_MINIMAL}", + f"NAME={service_name}", + f"POSTGRESQL_VERSION={VARS.PSQL_VERSION}", + ], + ) + assert self.oc_api.is_template_deployed(name_in_template=service_name) + assert self.oc_api.check_response_inside_cluster( + name_in_template=service_name, + expected_output="Welcome to your Django application on OpenShift", + ) diff --git a/test/test_ocp_helm_python_django_app.py b/test/test_ocp_helm_python_django_app.py new file mode 100644 index 00000000..9980f70f --- /dev/null +++ b/test/test_ocp_helm_python_django_app.py @@ -0,0 +1,41 @@ +from container_ci_suite.helm import HelmChartsAPI + +from conftest import skip_helm_charts_tests, VARS + + +class TestHelmPythonDjangoAppTemplate: + def setup_method(self): + package_name = "redhat-python-django-application" + self.hc_api = HelmChartsAPI( + path=VARS.TEST_DIR, + package_name=package_name, + tarball_dir=VARS.TEST_DIR, + shared_cluster=True, + ) + self.hc_api.clone_helm_chart_repo( + repo_url="https://github.com/sclorg/helm-charts", + repo_name="helm-charts", + subdir="charts/redhat", + ) + + def teardown_method(self): + self.hc_api.delete_project() + + def test_django_application_helm_test(self): + skip_helm_charts_tests() + self.hc_api.package_name = "redhat-python-imagestreams" + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation() + self.hc_api.package_name = "redhat-python-django-application" + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation( + values={ + "python_version": f"{VARS.VERSION}{VARS.TAG}", + "namespace": self.hc_api.namespace, + "source_repository_ref": VARS.BRANCH_TO_TEST, + } + ) + assert self.hc_api.is_s2i_pod_running(pod_name_prefix="django-example") + assert self.hc_api.test_helm_chart( + expected_str=["Welcome to your Django application"] + ) diff --git a/test/test_ocp_helm_python_django_psql_persistent.py b/test/test_ocp_helm_python_django_psql_persistent.py new file mode 100644 index 00000000..df388c84 --- /dev/null +++ b/test/test_ocp_helm_python_django_psql_persistent.py @@ -0,0 +1,44 @@ +from container_ci_suite.helm import HelmChartsAPI + +from conftest import skip_helm_charts_tests, VARS + + +class TestHelmPythonDjangoPsqlTemplate: + def setup_method(self): + package_name = "redhat-django-psql-persistent" + self.hc_api = HelmChartsAPI( + path=VARS.TEST_DIR, + package_name=package_name, + tarball_dir=VARS.TEST_DIR, + shared_cluster=True, + ) + self.hc_api.clone_helm_chart_repo( + repo_url="https://github.com/sclorg/helm-charts", + repo_name="helm-charts", + subdir="charts/redhat", + ) + + def teardown_method(self): + self.hc_api.delete_project() + + def test_django_psql_helm_test(self): + skip_helm_charts_tests() + self.hc_api.package_name = "redhat-postgresql-imagestreams" + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation() + self.hc_api.package_name = "redhat-python-imagestreams" + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation() + self.hc_api.package_name = "redhat-django-psql-persistent" + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation( + values={ + "python_version": f"{VARS.VERSION}{VARS.TAG}", + "namespace": self.hc_api.namespace, + "source_repository_ref": VARS.BRANCH_TO_TEST, + } + ) + assert self.hc_api.is_s2i_pod_running(pod_name_prefix="django-psql") + assert self.hc_api.test_helm_chart( + expected_str=["Welcome to your Django application"] + ) diff --git a/test/test_ocp_helm_python_imagestreams.py b/test/test_ocp_helm_python_imagestreams.py new file mode 100644 index 00000000..9ca0424d --- /dev/null +++ b/test/test_ocp_helm_python_imagestreams.py @@ -0,0 +1,42 @@ +import pytest + +from container_ci_suite.helm import HelmChartsAPI + +from conftest import VARS + + +class TestHelmRHELPythonImageStreams: + def setup_method(self): + package_name = "redhat-python-imagestreams" + self.hc_api = HelmChartsAPI( + path=VARS.TEST_DIR, + package_name=package_name, + tarball_dir=VARS.TEST_DIR, + shared_cluster=True, + ) + self.hc_api.clone_helm_chart_repo( + repo_url="https://github.com/sclorg/helm-charts", + repo_name="helm-charts", + subdir="charts/redhat", + ) + + def teardown_method(self): + self.hc_api.delete_project() + + @pytest.mark.parametrize( + "version,registry", + [ + ( + "3.12-minimal-ubi10", + "registry.redhat.io/ubi10/python-312-minimal:latest", + ), + ("3.12-ubi9", "registry.redhat.io/ubi9/python-312:latest"), + ("3.12-ubi8", "registry.redhat.io/ubi8/python-312:latest"), + ("3.9-ubi9", "registry.redhat.io/ubi9/python-39:latest"), + ("3.6-ubi8", "registry.redhat.io/ubi8/python-36:latest"), + ], + ) + def test_package_imagestream(self, version, registry): + assert self.hc_api.helm_package() + assert self.hc_api.helm_installation() + assert self.hc_api.check_imagestreams(version=version, registry=registry) diff --git a/test/test_ocp_imagestreams_quickstart.py b/test/test_ocp_imagestreams_quickstart.py new file mode 100644 index 00000000..50138108 --- /dev/null +++ b/test/test_ocp_imagestreams_quickstart.py @@ -0,0 +1,63 @@ +import pytest + +from container_ci_suite.openshift import OpenShiftAPI + +from conftest import VARS + + +class TestImagestreamsQuickstart: + def setup_method(self): + self.oc_api = OpenShiftAPI( + pod_name_prefix=f"python-{VARS.SHORT_VERSION}-test", + version=VARS.VERSION_NO_MINIMAL, + shared_cluster=True, + ) + + def teardown_method(self): + self.oc_api.delete_project() + + @pytest.mark.parametrize( + "template", ["django.json", "django-postgresql-persistent.json"] + ) + def test_python_template_inside_cluster(self, template): + if VARS.VERSION in ("3.14", "3.14-minimal"): + pytest.skip(f"Testing templates for {VARS.VERSION} is not yet GA.") + if self.oc_api.shared_cluster: + assert self.oc_api.upload_image_to_external_registry( + VARS.DEPLOYED_PSQL_IMAGE, VARS.IMAGE_TAG + ) + else: + assert self.oc_api.upload_image(VARS.DEPLOYED_PSQL_IMAGE, VARS.IMAGE_TAG) + service_name = f"python-{VARS.SHORT_VERSION}-test" + template_url = self.oc_api.get_raw_url_for_json( + container="django-ex", + dir="openshift/templates", + filename=template, + branch=VARS.BRANCH_TO_TEST, + ) + openshift_args = [ + f"SOURCE_REPOSITORY_REF={VARS.BRANCH_TO_TEST}", + f"PYTHON_VERSION={VARS.VERSION_NO_MINIMAL}", + f"NAME={service_name}", + ] + if template != "django.json": + openshift_args = [ + f"SOURCE_REPOSITORY_REF={VARS.BRANCH_TO_TEST}", + f"POSTGRESQL_VERSION={VARS.PSQL_VERSION}", + f"PYTHON_VERSION={VARS.VERSION_NO_MINIMAL}", + f"NAME={service_name}", + "DATABASE_USER=testu", + "DATABASE_PASSWORD=testp", + ] + assert self.oc_api.imagestream_quickstart( + imagestream_file="imagestreams/python-rhel.json", + template_file=template_url, + image_name=VARS.IMAGE_NAME, + name_in_template="python", + openshift_args=openshift_args, + ) + assert self.oc_api.is_template_deployed(name_in_template=service_name) + assert self.oc_api.check_response_inside_cluster( + name_in_template=service_name, + expected_output="Welcome to your Django application on OpenShift", + ) diff --git a/test/test_ocp_latest_imagestreams.py b/test/test_ocp_latest_imagestreams.py new file mode 100644 index 00000000..ef1cd424 --- /dev/null +++ b/test/test_ocp_latest_imagestreams.py @@ -0,0 +1,14 @@ +from container_ci_suite.imagestreams import ImageStreamChecker + +from conftest import VARS + + +# Replacement with 'test_latest_imagestreams' +class TestLatestImagestreams: + def setup_method(self): + self.isc = ImageStreamChecker(working_dir=VARS.TEST_DIR.parent.parent) + + def test_latest_imagestream(self): + self.latest_version = self.isc.get_latest_version() + assert self.latest_version + self.isc.check_imagestreams(self.latest_version) diff --git a/test/test_ocp_python_ex_standalone.py b/test/test_ocp_python_ex_standalone.py new file mode 100644 index 00000000..4973f7fd --- /dev/null +++ b/test/test_ocp_python_ex_standalone.py @@ -0,0 +1,30 @@ +from container_ci_suite.openshift import OpenShiftAPI + +from conftest import VARS + + +# Replacement with 'test_python_s2i_app_ex_standalone' +class TestPythonExTemplate: + def setup_method(self): + self.oc_api = OpenShiftAPI( + pod_name_prefix=f"python-{VARS.SHORT_VERSION}-test", + version=VARS.VERSION, + shared_cluster=True, + ) + + def teardown_method(self): + self.oc_api.delete_project() + + def test_python_ex_template_inside_cluster(self): + service_name = f"python-{VARS.SHORT_VERSION}-test" + assert self.oc_api.deploy_s2i_app( + image_name=VARS.IMAGE_NAME, + app="https://github.com/sclorg/s2i-python-container.git", + context="examples/standalone-test-app", + service_name=service_name, + ) + assert self.oc_api.is_template_deployed(name_in_template=service_name) + assert self.oc_api.check_response_inside_cluster( + name_in_template=service_name, + expected_output="Hello World from standalone WSGI application!", + ) diff --git a/test/test_ocp_python_ex_template.py b/test/test_ocp_python_ex_template.py new file mode 100644 index 00000000..fe557ce9 --- /dev/null +++ b/test/test_ocp_python_ex_template.py @@ -0,0 +1,30 @@ +from container_ci_suite.openshift import OpenShiftAPI + +from conftest import VARS + + +# Replacement with 'test_python_s2i_app_ex' +class TestPythonExTemplate: + def setup_method(self): + self.oc_api = OpenShiftAPI( + pod_name_prefix=f"python-{VARS.SHORT_VERSION}-test", + version=VARS.VERSION_NO_MINIMAL, + shared_cluster=True, + ) + + def teardown_method(self): + self.oc_api.delete_project() + + def test_python_ex_template_inside_cluster(self): + service_name = f"python-{VARS.SHORT_VERSION}-test" + assert self.oc_api.deploy_s2i_app( + image_name=VARS.IMAGE_NAME, + app=f"https://github.com/sclorg/django-ex.git#{VARS.BRANCH_TO_TEST}", + context=".", + service_name=service_name, + ) + assert self.oc_api.is_template_deployed(name_in_template=service_name) + assert self.oc_api.check_response_inside_cluster( + name_in_template=service_name, + expected_output="Welcome to your Django application on OpenShift", + )