Plugin images¶
Plugins are Python distributions that must be installed in the image of
proTES: nothing is installed at runtime, and the Helm chart keeps the root
file system of the containers read-only. The published images of proTES
have FOCA's built-in plugins and two routing plugins from foca-plugins
(generic.routing.random, enabled by default, and tes.routing.distance,
which admins enable explicitly, see Plugins);
there is no image with all plugins, as plugins come from several
repositories, in many combinations. To deploy other plugins, or to add data
that plugins need (e.g., the GeoIP database of the offline provider of
tes.routing.distance), build an image derived from a release image of proTES with
the recipe of this page: it installs exactly the distribution files that you
pinned, reviewed and verified, and nothing else. Then enable the plugins in
the config (see Plugins): installed is not enabled.
Plugins of foca-plugins
The plugins of ELIXIR On-Cloud, e.g., tes.routing.distance, are
developed in the repository foca-plugins and published to PyPI as
single distributions (foca-plugin-<group>-<subgroup>-<name>, e.g.,
foca-plugin-tes-routing-distance and
foca-plugin-generic-routing-random) and as release trains (extras of
the distribution foca-plugins, e.g., foca-plugins[tes-routing]). See
its
documentation.
The images of proTES contain the two routing plugins already (the
release train foca-plugins, pinned in pyproject.toml, with exact
versions and hashes in the repository's uv.lock, from which the image
build installs them hash-checked, as all dependencies of proTES, see
Dependencies); don't list them in
plugins.in. The recipe works the same for plugins of any repository, and CI
tests it with a test plugin (see Testing the
recipe).
The commands below run in a directory of their own (the build context),
with Docker and the files resolve.sh, download.sh, common.sh,
Dockerfile and Dockerfile.dockerignore of examples/plugin-image/ of
the proTES
repository (of the release that you build on), copied to the directory.
The proTES image has no pip and no uv: the scripts and the Dockerfile
mount uv, from the image pinned by digest in the Dockerfile, so that the
plugins are resolved for the image's Python version, platform and installed
packages, and uv never stays in an image.
Base image¶
Build on a release image of proTES, by digest (see Verifying images and charts to check it):
docker buildx imagetools inspect docker.io/elixircloud/protes:0.6.0 # shows the digest
export PROTES_IMAGE=docker.io/elixircloud/protes:0.6.0@sha256:<digest>
Pinning plugins¶
List the plugins in plugins.in, as requirements: single distributions
or a release train, e.g.:
Resolve them against the image:
resolve.sh runs uv pip compile inside the image, with the image's
installed packages as constraints, and writes the distributions that the
image lacks (the plugins and their dependencies), with their exact versions
and the hashes of their wheels, to plugins.txt. Options of docker run
follow the file names, e.g., --env UV_DEFAULT_INDEX=<index> for another
package index, which must also serve the dependencies of the plugins that
the image has (e.g., FOCA), as uv doesn't take them from the image. They go
to the docker run that resolves, so run the scripts on the image's
architecture: --platform doesn't work.
The constraints keep the packages of the image as they are: resolve.sh
fails (with uv's errors) and removes plugins.txt if a plugin needs other
versions of them (e.g., another FOCA version), or if a distribution can't be
found; choose plugin versions that work with those of the image.
Review plugins.txt and keep it, e.g., in the repository of your
deployment: it is the complete list of what the image adds. If it is empty,
the image has all the plugins already: skip the remaining steps.
Downloading and verifying¶
Download the wheels:
download.sh runs pip for this one command, as uv has no download command:
a version pinned in the script, without a hash check of its own, from uv's
temporary environment, not from the image. Options of docker run follow
the directory, e.g., --env PIP_INDEX_URL=<index> for the wheels and --env
UV_DEFAULT_INDEX=<index> for pip itself. pip checks the hashes of
plugins.txt, and the image build checks them again. Before the build, also check where
the wheels come from: the plugins of foca-plugins are published with PEP
740 attestations and GitHub artifact
attestations of their build provenance (see Verifying
distributions).
For example, check the GitHub artifact attestation of each of their wheels,
with the repository that publishes the plugin:
Check other plugins and dependencies as their projects document.
Building¶
Build the image:
docker build --build-arg PROTES_IMAGE="$PROTES_IMAGE" \
--tag registry.example.org/protes-plugins:0.6.0-1 .
The Dockerfile:
# check=skip=InvalidDefaultArgInFrom
## (The base image has no default: builds name the release image explicitly)
## Image with plugins, derived from a proTES release image (see
## `docs/plugin-images.md`). The build context holds `plugins.txt`, the
## distributions to install, each pinned with its hash, and `wheels/`, their
## files, downloaded and verified before the build; the build installs these
## files only (no index, no other dependencies), as root, then checks that
## the dependencies of all installed distributions are met. Build it with
## the release image, by digest:
## docker build --file Dockerfile \
## --build-arg PROTES_IMAGE=docker.io/elixircloud/protes:<version>@sha256:<digest> \
## --tag <registry>/protes-plugins:<tag> <context>
## The smoke test builds it in CI with a test plugin (`tests/smoke/plugins.sh`)
ARG PROTES_IMAGE
FROM ghcr.io/astral-sh/uv:0.13.8@sha256:5cc1805e2426484abca4ad46ea783bab281fdb225fd82d8ce6d66a81254bac85 AS uv
FROM ${PROTES_IMAGE}
USER root
RUN --mount=from=uv,source=/uv,target=/usr/local/bin/uv \
--mount=type=bind,source=plugins.txt,target=/tmp/plugins/plugins.txt \
--mount=type=bind,source=wheels,target=/tmp/plugins/wheels \
export UV_NO_CACHE=1 UV_PYTHON_DOWNLOADS=never \
&& uv pip install --system --no-index --no-deps --require-hashes \
--only-binary :all: --compile-bytecode --find-links /tmp/plugins/wheels \
--requirement /tmp/plugins/plugins.txt \
&& uv pip check --system \
&& rm -f /tmp/uv-*.lock
USER 1000
The build installs the wheels of wheels/ that plugins.txt lists, and
nothing else: without the package index and without further dependencies
(--no-index --no-deps --require-hashes). It fails if a hash differs or a
dependency of an installed distribution is missing (uv pip check). The image
runs as user 1000, as that of proTES. Only plugins.txt and wheels/ are
sent to the build (Dockerfile.dockerignore), so that other files of the
directory, e.g., .env files, never reach it.
Scan the image for known vulnerabilities (e.g., with
Trivy: trivy image <image>), push it to your
registry and sign it (e.g., with cosign:
cosign sign <image>@<digest>), as the workflows of proTES do for its
images.
Deploying¶
Deploy the image and enable the plugins, e.g., in the values file of the Helm chart (see Configuration of the chart's README):
protes:
image: registry.example.org/protes-plugins@sha256:<digest>
tag: ""
imagePullPolicy: IfNotPresent
plugins:
available:
org.example.plugin:
distribution: foca-plugin-<group>-<subgroup>-<name>
# The distance routing plugin of the image (not enabled by default):
# locate addresses offline with a GeoIP database that your image adds,
# e.g., by a `COPY` of your own (see the plugin's README), and put it
# into the pipeline, e.g., before `generic.routing.random`
tes.routing.distance:
distribution: foca-plugin-tes-routing-distance
config:
provider: offline
database: /opt/geoip/dbip-city-lite.mmdb
The app refuses to start if an enabled plugin isn't installed, doesn't match
its pins (distribution, version), or doesn't support the TES API: a
wrong image fails at once, not with the first task.
Rebuild the image for every release of proTES that you deploy, and resolve
plugins.in against the new image again (see Pinning
plugins), as its packages change. Plugins declare the
FOCA versions that they support (e.g., foca>=0.22.0,<0.23), so resolving
them against an image with another FOCA version fails.
Local builds¶
For development, the Dockerfile of proTES installs plugins with uv, given
as requirements in the build argument PLUGINS (empty by default), from the
package index and with their dependencies; Docker Compose passes the host's
PLUGINS:
Enable them in the config overlay (e.g., config.dev.yaml). Unlike the
dependencies of proTES, which the image build installs from the lock, these
plugins are not pinned by hash nor reviewed: use the recipe above for
images that you deploy. PLUGINS is split at spaces (write specifiers without
spaces, e.g., name>=1.0) and recorded in the image's history: pass
requirement specifiers only, never credentials (e.g., of a private package
index).
Testing the recipe¶
The smoke test of CI (tests/smoke/plugins.sh, see
Development) builds the image of the recipe from the app's
image with a test plugin (tests/smoke/plugin/, which tags each new task),
pinned by hash with resolve.sh and downloaded with download.sh (the
plugin from a local directory, its dependencies from the package index, and
pip itself too), checks that a wrong hash fails the build, runs proTES with
it enabled, and checks that new tasks get its tag.