From 1c2be2c9b05ca69540f7b67ddaa0d4a709d14cee Mon Sep 17 00:00:00 2001 From: kavehtoyser Date: Fri, 24 Apr 2026 15:32:32 +0200 Subject: [PATCH] Add python312-slim builder image and make it the new :latest The daeploy Manager was updated to Python 3.12 and its dependencies now require Python >= 3.10 (e.g. fastapi 0.135.3). Services built on the python38-slim image can no longer install the current daeploy wheel, so add a python312-slim builder and publish it as :latest. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci_checks.yml | 2 + .github/workflows/docker_image_release.yml | 3 +- python312-slim/Dockerfile | 35 ++++ python312-slim/Makefile | 10 ++ python312-slim/README.md | 105 +++++++++++ python312-slim/s2i/bin/assemble | 26 +++ python312-slim/s2i/bin/run | 43 +++++ python312-slim/s2i/bin/usage | 12 ++ python312-slim/test/run | 169 ++++++++++++++++++ python312-slim/test/test-app/.s2i/environment | 1 + python312-slim/test/test-app/.s2iignore | 8 + python312-slim/test/test-app/README.md | 37 ++++ python312-slim/test/test-app/requirements.txt | 1 + python312-slim/test/test-app/service.py | 17 ++ .../test-app/tests/base_functionality_test.py | 27 +++ .../test/test-app/tests/user_test.py | 16 ++ 16 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 python312-slim/Dockerfile create mode 100644 python312-slim/Makefile create mode 100644 python312-slim/README.md create mode 100755 python312-slim/s2i/bin/assemble create mode 100755 python312-slim/s2i/bin/run create mode 100755 python312-slim/s2i/bin/usage create mode 100755 python312-slim/test/run create mode 100644 python312-slim/test/test-app/.s2i/environment create mode 100644 python312-slim/test/test-app/.s2iignore create mode 100644 python312-slim/test/test-app/README.md create mode 100644 python312-slim/test/test-app/requirements.txt create mode 100644 python312-slim/test/test-app/service.py create mode 100644 python312-slim/test/test-app/tests/base_functionality_test.py create mode 100644 python312-slim/test/test-app/tests/user_test.py diff --git a/.github/workflows/ci_checks.yml b/.github/workflows/ci_checks.yml index 2de01f0..b61a945 100644 --- a/.github/workflows/ci_checks.yml +++ b/.github/workflows/ci_checks.yml @@ -25,3 +25,5 @@ jobs: run: make test -C python38-slim/ - name: Test python 3.9 image run: make test -C python39-slim/ + - name: Test python 3.12 image + run: make test -C python312-slim/ diff --git a/.github/workflows/docker_image_release.yml b/.github/workflows/docker_image_release.yml index 8a873ec..9378066 100644 --- a/.github/workflows/docker_image_release.yml +++ b/.github/workflows/docker_image_release.yml @@ -21,7 +21,8 @@ jobs: - name: Docker build and push run: | - docker build -t daeploy/s2i-python:3.8-slim-${{ github.event.release.tag_name }} -t daeploy/s2i-python:latest ./python38-slim + docker build -t daeploy/s2i-python:3.12-slim-${{ github.event.release.tag_name }} -t daeploy/s2i-python:latest ./python312-slim + docker build -t daeploy/s2i-python:3.8-slim-${{ github.event.release.tag_name }} ./python38-slim docker build -t daeploy/s2i-python:3.6-slim-${{ github.event.release.tag_name }} ./python36-slim docker build -t daeploy/s2i-python:3.7-slim-${{ github.event.release.tag_name }} ./python37-slim docker build -t daeploy/s2i-python:3.9-slim-${{ github.event.release.tag_name }} ./python39-slim diff --git a/python312-slim/Dockerfile b/python312-slim/Dockerfile new file mode 100644 index 0000000..59a893e --- /dev/null +++ b/python312-slim/Dockerfile @@ -0,0 +1,35 @@ +# daeploy/s2i-python +FROM python:3.12-slim + +LABEL maintainer="Viking Analytics AB" + +ENV BUILDER_VERSION=1.0 \ + APP_ROOT="/opt/app-root" \ + HOME="/opt/app-root/src" \ + STI_SCRIPTS_PATH="/usr/libexec/s2i" + +# Set labels used in OpenShift to describe the builder image +LABEL io.k8s.description="Platform for building lightweight Daeploy images" \ + io.k8s.display-name="Daeploy python builder" \ + io.openshift.expose-services="8080:http" \ + io.openshift.tags="builder,daeploy,python." \ + io.openshift.s2i.scripts-url="image://${STI_SCRIPTS_PATH}" + +# Setup virtualenv +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy the S2I scripts to the image +COPY ./s2i/bin/ $STI_SCRIPTS_PATH + +# Set the default port for applications built using this image +EXPOSE 8080 + +# Setup file system +RUN mkdir -p ${APP_ROOT} && \ + mkdir -p ${HOME} + +WORKDIR $HOME + +# Set the default CMD for the image +CMD ["$STI_SCRIPTS_PATH/usage"] diff --git a/python312-slim/Makefile b/python312-slim/Makefile new file mode 100644 index 0000000..b7249ed --- /dev/null +++ b/python312-slim/Makefile @@ -0,0 +1,10 @@ +IMAGE_NAME = daeploy/s2i-python + +.PHONY: build +build: + docker build -t $(IMAGE_NAME) . + +.PHONY: test +test: + docker build -t $(IMAGE_NAME)-candidate . + IMAGE_NAME=$(IMAGE_NAME)-candidate test/run diff --git a/python312-slim/README.md b/python312-slim/README.md new file mode 100644 index 0000000..99ab564 --- /dev/null +++ b/python312-slim/README.md @@ -0,0 +1,105 @@ + +# Creating a basic S2I builder image + +## Getting started + +### Files and Directories + +| File | Required? | Description | +|------------------------|-----------|--------------------------------------------------------------| +| Dockerfile | Yes | Defines the base builder image | +| s2i/bin/assemble | Yes | Script that builds the application | +| s2i/bin/usage | No | Script that prints the usage of the builder | +| s2i/bin/run | Yes | Script that runs the application | +| s2i/bin/save-artifacts | No | Script for incremental builds that saves the built artifacts | +| test/run | No | Test script for the builder image | +| test/test-app | Yes | Test application source code | + +#### Dockerfile + +Create a *Dockerfile* that installs all of the necessary tools and libraries that are needed to build and run our application. This file will also handle copying the s2i scripts into the created image. + +#### S2I scripts + +##### assemble + +Create an *assemble* script that will build our application, e.g.: + +- build python modules +- bundle install ruby gems +- setup application specific configuration + +The script can also specify a way to restore any saved artifacts from the previous image. + +##### run + +Create a *run* script that will start the application. + +##### save-artifacts (optional) + +Create a *save-artifacts* script which allows a new build to reuse content from a previous version of the application image. + +##### usage (optional) + +Create a *usage* script that will print out instructions on how to use the image. + +##### Make the scripts executable + +Make sure that all of the scripts are executable by running *chmod +x s2i/bin/** + +#### Create the builder image + +The following command will create a builder image named daeploy/s2i-python based on the Dockerfile that was created previously. + +```bash +docker build -t daeploy/s2i-python . +``` + +The builder image can also be created by using the *make* command since a *Makefile* is included. + +Once the image has finished building, the command *s2i usage daeploy/s2i-python* will print out the help info that was defined in the *usage* script. + +#### Testing the builder image + +The builder image can be tested using the following commands: + +```bash +docker build -t daeploy/s2i-python-candidate . +IMAGE_NAME=daeploy/s2i-python-candidate test/run +``` + +The builder image can also be tested by using the *make test* command since a *Makefile* is included. + +#### Creating the application image + +The application image combines the builder image with your applications source code, which is served using whatever application is installed via the *Dockerfile*, compiled using the *assemble* script, and run using the *run* script. +The following command will create the application image: + +```bash +s2i build test/test-app daeploy/s2i-python daeploy/s2i-python-app +---> Building and installing application from source... +``` + +Using the logic defined in the *assemble* script, s2i will now create an application image using the builder image as a base and including the source code from the test/test-app directory. + +#### Running the application image + +Running the application image is as simple as invoking the docker run command: + +```bash +docker run -d -p 8080:8080 daeploy/s2i-python-app +``` + +The application, which consists of a simple static web page, should now be accessible at [http://localhost:8080](http://localhost:8080). + +#### Using the saved artifacts script + +Rebuilding the application using the saved artifacts can be accomplished using the following command: + +```bash +s2i build --incremental=true test/test-app nginx-centos7 nginx-app +---> Restoring build artifacts... +---> Building and installing application from source... +``` + +This will run the *save-artifacts* script which includes the custom code to backup the currently running application source, rebuild the application image, and then re-deploy the previously saved source using the *assemble* script. diff --git a/python312-slim/s2i/bin/assemble b/python312-slim/s2i/bin/assemble new file mode 100755 index 0000000..7383e5d --- /dev/null +++ b/python312-slim/s2i/bin/assemble @@ -0,0 +1,26 @@ +#!/bin/bash +# +# S2I assemble script for the 'daeploy/s2i-python' image. +# The 'assemble' script builds your application source so that it is ready to run. +# +# For more information refer to the documentation: +# https://github.com/openshift/source-to-image/blob/master/docs/builder_image.md +# + +# If the 'daeploy/s2i-python' assemble script is executed with the '-h' flag, print the usage. +if [[ "$1" == "-h" ]]; then + exec $STI_SCRIPTS_PATH/usage +fi + +echo "---> Installing application source ..." +shopt -s dotglob +mv /tmp/src/* "$HOME" + +# Install dependencies +echo "---> Upgrading pip and setuptools" +pip install -U pip + +if [[ -f requirements.txt ]]; then + echo "---> Installing dependencies..." + python -m pip install -r requirements.txt +fi diff --git a/python312-slim/s2i/bin/run b/python312-slim/s2i/bin/run new file mode 100755 index 0000000..0a2f326 --- /dev/null +++ b/python312-slim/s2i/bin/run @@ -0,0 +1,43 @@ +#!/bin/bash +# +# S2I run script for the 'daeploy/s2i-python' image. +# The run script executes the server that runs your application. +# +# For more information see the documentation: +# https://github.com/openshift/source-to-image/blob/master/docs/builder_image.md +# + +cd $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_ROOT/src/$APP_SCRIPT" + fi + chmod +x "$APP_SCRIPT" + "$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) ..." + python "$APP_FILE" + elif [[ -z "$APP_FILE_DEFAULT" ]]; then + echo "ERROR: file '$APP_FILE' not found." && exit 1 + fi +fi diff --git a/python312-slim/s2i/bin/usage b/python312-slim/s2i/bin/usage new file mode 100755 index 0000000..bd5a7de --- /dev/null +++ b/python312-slim/s2i/bin/usage @@ -0,0 +1,12 @@ +#!/bin/bash +cat < daeploy/s2i-python + +You can then run the resulting image via: +docker run +EOF diff --git a/python312-slim/test/run b/python312-slim/test/run new file mode 100755 index 0000000..d856858 --- /dev/null +++ b/python312-slim/test/run @@ -0,0 +1,169 @@ +#!/bin/bash +# +# The 'run' performs a simple test that verifies the S2I image. +# The main focus here is to exercise the S2I scripts. +# +# For more information see the documentation: +# https://github.com/openshift/source-to-image/blob/master/docs/builder_image.md +# +# IMAGE_NAME specifies a name of the candidate image used for testing. +# The image has to be available before this script is executed. +# +IMAGE_NAME=${IMAGE_NAME-daeploy/s2i-python-candidate} + +# Determining system utility executables (darwin compatibility check) +READLINK_EXEC="readlink -zf" +MKTEMP_EXEC="mktemp --suffix=.cid" +if [[ "$OSTYPE" =~ 'darwin' ]]; then + READLINK_EXEC="readlink" + MKTEMP_EXEC="mktemp" + ! type -a "greadlink" &>"/dev/null" || READLINK_EXEC="greadlink" + ! type -a "gmktemp" &>"/dev/null" || MKTEMP_EXEC="gmktemp" +fi + +_dir="$(dirname "${BASH_SOURCE[0]}")" +test_dir="$($READLINK_EXEC ${_dir} || echo ${_dir})" +image_dir=$($READLINK_EXEC ${test_dir}/.. || echo ${test_dir}/..) +scripts_url="${image_dir}/.s2i/bin" +cid_file=$($MKTEMP_EXEC -u) + +# Since we built the candidate image locally, we don't want S2I to attempt to pull +# it from Docker hub +s2i_args="--pull-policy=never --loglevel=2" + +# Port the image exposes service to be tested +test_port=8000 + +image_exists() { + docker inspect $1 &>/dev/null +} + +container_exists() { + image_exists $(cat $cid_file) +} + +container_ip() { + docker inspect --format="{{(index .NetworkSettings.Ports \"$test_port/tcp\" 0).HostIp }}" $(cat $cid_file) | sed 's/0.0.0.0/localhost/' +} + +container_port() { + docker inspect --format="{{(index .NetworkSettings.Ports \"$test_port/tcp\" 0).HostPort }}" "$(cat "${cid_file}")" +} + +run_s2i_build() { + s2i build --incremental=true ${s2i_args} ${test_dir}/test-app ${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 + # s2i build requires the application is a valid 'Git' repository + pushd ${test_dir}/test-app >/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_s2i_build +} + +run_test_application() { + docker run --rm --cidfile=${cid_file} -p ${test_port}:${test_port} ${IMAGE_NAME}-testapp +} + +cleanup() { + if [ -f $cid_file ]; then + if container_exists; then + docker stop $(cat $cid_file) + fi + fi + if image_exists ${IMAGE_NAME}-testapp; then + docker rmi ${IMAGE_NAME}-testapp + fi +} + +check_result() { + local result="$1" + if [[ "$result" != "0" ]]; then + echo "S2I image '${IMAGE_NAME}' test FAILED (exit code: ${result})" + cleanup + exit $result + fi +} + +wait_for_cid() { + local max_attempts=10 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + [ -f $cid_file ] && break + echo "Waiting for container to start..." + attempt=$(( $attempt + 1 )) + sleep $sleep_time + done +} + +test_usage() { + echo "Testing 's2i usage'..." + s2i usage ${s2i_args} ${IMAGE_NAME} &>/dev/null +} + +http_call() { + local url="$1" + curl -X 'POST' \ + "$url" \ + -w %{http_code} \ + -s \ + -o /dev/null \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "name": "Snorre Sturlasson" + }' +} + +test_connection() { + echo "Testing HTTP connection (http://$(container_ip):$(container_port))" + local max_attempts=10 + local sleep_time=1 + local attempt=1 + local result=1 + while [ $attempt -le $max_attempts ]; do + echo "Sending POST request http://$(container_ip):$(container_port)/hello" + response_code=$(http_call http://$(container_ip):$(container_port)/hello) + 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 +} + +# Build the application image twice to ensure the 'save-artifacts' and +# 'restore-artifacts' scripts are working properly +prepare +run_s2i_build +check_result $? + +# Verify the 'usage' script is working properly +test_usage +check_result $? + +# Verify that the HTTP connection can be established to test application container +run_test_application & + +# Wait for the container to write its CID file +wait_for_cid + +test_connection +check_result $? + +cleanup +echo "Test successful" diff --git a/python312-slim/test/test-app/.s2i/environment b/python312-slim/test/test-app/.s2i/environment new file mode 100644 index 0000000..16fd266 --- /dev/null +++ b/python312-slim/test/test-app/.s2i/environment @@ -0,0 +1 @@ +APP_FILE = service.py \ No newline at end of file diff --git a/python312-slim/test/test-app/.s2iignore b/python312-slim/test/test-app/.s2iignore new file mode 100644 index 0000000..2e2177d --- /dev/null +++ b/python312-slim/test/test-app/.s2iignore @@ -0,0 +1,8 @@ +# In this file you can add files/folders to ignore when deploying +# Works similar to a .gitignore file + +# Ignore the .git folder to not only use committed code +.git/ + +# Test directory is not needed in production +tests/ diff --git a/python312-slim/test/test-app/README.md b/python312-slim/test/test-app/README.md new file mode 100644 index 0000000..116bd7a --- /dev/null +++ b/python312-slim/test/test-app/README.md @@ -0,0 +1,37 @@ +# Daeploy project: my_project + +## Creating the service + +The ```service.py``` contains an example service and shows how to use the daeploy SDK. + +This example shows: + +- How to initiate a daeploy service +- How to register changeable parameters +- How to register functions as entrypoints +- How to log from the service +- How to send notifications from the service +- How to start the service + +If the service requires external packages, they should be specified in the `requirements.txt` file. + +## Deploying the service + +The service can be deployed using the daeploy command-line interface: + +From a local directory or tarball (.tar.gz): + +```daeploy deploy myservice 0.0.1 ``` + +From a git repository: + +```daeploy deploy myservice 0.0.1 --git ``` + +## Documentation + +Full documentation of the SDK and the CLI can be found here: [https://vikinganalytics.github.io/daeploy-docs/](https://vikinganalytics.github.io/daeploy-docs/) + +## Extra + +If you want to change name of the service file ```service.py```, or deploy another file as the +daeploy service then you have to change the ```APP_FILE``` key in the ```.s2i/environment``` file. diff --git a/python312-slim/test/test-app/requirements.txt b/python312-slim/test/test-app/requirements.txt new file mode 100644 index 0000000..e416d97 --- /dev/null +++ b/python312-slim/test/test-app/requirements.txt @@ -0,0 +1 @@ +daeploy \ No newline at end of file diff --git a/python312-slim/test/test-app/service.py b/python312-slim/test/test-app/service.py new file mode 100644 index 0000000..e129854 --- /dev/null +++ b/python312-slim/test/test-app/service.py @@ -0,0 +1,17 @@ +import logging +from daeploy import service + +logger = logging.getLogger(__name__) + +service.add_parameter("greeting_phrase", "Hello") + + +@service.entrypoint +def hello(name: str) -> str: + greeting_phrase = service.get_parameter("greeting_phrase") + logger.info(f"Greeting someone with the name: {name}") + return f"{greeting_phrase} {name}" + + +if __name__ == "__main__": + service.run() diff --git a/python312-slim/test/test-app/tests/base_functionality_test.py b/python312-slim/test/test-app/tests/base_functionality_test.py new file mode 100644 index 0000000..4971396 --- /dev/null +++ b/python312-slim/test/test-app/tests/base_functionality_test.py @@ -0,0 +1,27 @@ +from pathlib import Path + +# Change import if you have changed name of service.py +import service as app + +# Read service source file +SERVICE_DIR = Path(__file__).parent.parent +service_path = (SERVICE_DIR / app.__name__).with_suffix(".py") +with service_path.open("r") as file_handle: + service_code = file_handle.read() + + +def test_service_run(): + run_statement = "service.run()" + run_row = "" + + for item in service_code.split("\n"): + if run_statement in item: + run_row = item + + assert ( + run_statement in run_row + ), f"For the service to run, the service code must end with {run_statement}" + + run = run_row.find(run_statement) + comment = run_row.find("#") + assert comment == -1 or comment > run, f"{run_statement} commented out" diff --git a/python312-slim/test/test-app/tests/user_test.py b/python312-slim/test/test-app/tests/user_test.py new file mode 100644 index 0000000..d7d3953 --- /dev/null +++ b/python312-slim/test/test-app/tests/user_test.py @@ -0,0 +1,16 @@ +# Change import if you have changed name of service.py +from daeploy.testing import patch +import service + +# Test functions should always start with test_... +# Daeploy tests use the pytest package + +# Example tests: + + +def test_hello(): + # Test that service.hello("Bob") returns what is expected + assert service.hello("Bob") == "Hello Bob" + + +# Add your tests here