aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar ChrisJL <[email protected]>2023-08-29 19:35:53 +0100
committerGravatar GitHub <[email protected]>2023-08-29 19:35:53 +0100
commitf85116ab91229d3f45a8292978ffd2af7821c70e (patch)
treeff08fbd7ee351092948eca109b320d99f05338a1
parentMerge pull request #183 from python-discord/enforce-filesize-limits (diff)
parentInstall eval dependencies with --user & ensure user base var is set (diff)
Merge pull request #181 from python-discord/feat/158/multi-version
Install Multiple Python Versions in the Image
-rw-r--r--.dockerignore2
-rw-r--r--.github/CONTRIBUTING.md8
-rw-r--r--Dockerfile82
-rw-r--r--LICENSE-THIRD-PARTY (renamed from NOTICE)39
-rw-r--r--README.md12
-rw-r--r--config/snekbox.cfg33
-rw-r--r--deployment.yaml6
-rwxr-xr-xscripts/build_python.sh22
-rw-r--r--snekbox/nsjail.py4
-rw-r--r--tests/test_nsjail.py4
10 files changed, 135 insertions, 77 deletions
diff --git a/.dockerignore b/.dockerignore
index 6a360ff..ab815cd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -9,6 +9,6 @@
!snekbox/
!tests/
!LICENSE
-!NOTICE
+!LICENSE-THIRD-PARTY
!pyproject.toml
!README.md
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index d0a6921..1124b8e 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -62,6 +62,14 @@ Updating NsJail mainly involves two steps:
Other things to look out for are breaking changes to NsJail's config format, its command-line interface, or its logging format. Additionally, dependencies may have to be adjusted in the Dockerfile to get a new version to build or run.
+## Adding and Updating Python Interpreters
+
+Python interpreters are built using pyenv via the `scripts/build_python.sh` helper script. This script accepts a pyenv version specifier (`pyenv install --list`) and builds the interpreter in a version-specific directory under `/lang/python`. In the image, each minor version of a Python interpreter should have its own build stage and the resulting `/lang/python` directory can be copied from that stage into the `base` stage.
+
+When updating a patch version (e.g. 3.11.3 to 3.11.4), edit the existing build stage in the image for the minor version (3.11); do not add a new build stage. To have access to a new version, pyenv likely needs to be updated. To do so, change the tag in the `git clone` command in the image, but only for the build stage that needs access to the new version. Updating pyenv for all build stages will just cause unnecessary build cache invalidations.
+
+To change the default interpreter used by NsJail, update the target of the `/lang/python/default` symlink created in the `base` stage.
+
[readme]: ../README.md
[Dockerfile]: ../Dockerfile
[Compose v2]: https://docs.docker.com/compose/compose-v2/
diff --git a/Dockerfile b/Dockerfile
index 6355dac..6b5e2f6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,52 +1,73 @@
-# syntax=docker/dockerfile:1
-FROM python:3.11-slim-buster as builder
+# syntax=docker/dockerfile:1.4
+FROM buildpack-deps:buster as builder-nsjail
WORKDIR /nsjail
RUN apt-get -y update \
- && apt-get install -y \
- bison=2:3.3.* \
- flex=2.6.* \
- g++=4:8.3.* \
- gcc=4:8.3.* \
- git=1:2.20.* \
- libprotobuf-dev=3.6.* \
- libnl-route-3-dev=3.4.* \
- make=4.2.* \
- pkg-config=0.29-6 \
- protobuf-compiler=3.6.*
+ && apt-get install -y --no-install-recommends \
+ bison\
+ flex \
+ libprotobuf-dev\
+ libnl-route-3-dev \
+ protobuf-compiler \
+ && rm -rf /var/lib/apt/lists/*
+
RUN git clone -b master --single-branch https://github.com/google/nsjail.git . \
&& git checkout dccf911fd2659e7b08ce9507c25b2b38ec2c5800
RUN make
# ------------------------------------------------------------------------------
+FROM buildpack-deps:buster as builder-py-base
+
+ENV PYENV_ROOT=/pyenv \
+ PYTHON_CONFIGURE_OPTS='--disable-test-modules --enable-optimizations \
+ --with-lto --with-system-expat --without-ensurepip'
+
+RUN apt-get -y update \
+ && apt-get install -y --no-install-recommends \
+ libxmlsec1-dev \
+ tk-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --link scripts/build_python.sh /
+
+# ------------------------------------------------------------------------------
+FROM builder-py-base as builder-py-3_11
+RUN git clone -b v2.3.24 --depth 1 https://github.com/pyenv/pyenv.git $PYENV_ROOT \
+ && /build_python.sh 3.11.4
+
+# ------------------------------------------------------------------------------
+FROM builder-py-base as builder-py-3_12
+RUN git clone -b v2.3.24 --depth 1 https://github.com/pyenv/pyenv.git $PYENV_ROOT \
+ && /build_python.sh 3.12.0rc1
+
+# ------------------------------------------------------------------------------
FROM python:3.11-slim-buster as base
-# Everything will be a user install to allow snekbox's dependencies to be kept
-# separate from the packages exposed during eval.
-ENV PATH=/root/.local/bin:$PATH \
- PIP_DISABLE_PIP_VERSION_CHECK=1 \
- PIP_NO_CACHE_DIR=false \
- PIP_USER=1
+ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
+ PIP_NO_CACHE_DIR=false
RUN apt-get -y update \
- && apt-get install -y \
- gcc=4:8.3.* \
- git=1:2.20.* \
- libnl-route-3-200=3.4.* \
- libprotobuf17=3.6.* \
+ && apt-get install -y --no-install-recommends \
+ gcc \
+ git \
+ libnl-route-3-200 \
+ libprotobuf17 \
&& rm -rf /var/lib/apt/lists/*
-COPY --from=builder /nsjail/nsjail /usr/sbin/
-RUN chmod +x /usr/sbin/nsjail
+COPY --link --from=builder-nsjail /nsjail/nsjail /usr/sbin/
+COPY --link --from=builder-py-3_11 /lang/ /lang/
+COPY --link --from=builder-py-3_12 /lang/ /lang/
+
+RUN chmod +x /usr/sbin/nsjail \
+ && ln -s /lang/python/3.11/ /lang/python/default
# ------------------------------------------------------------------------------
FROM base as venv
-COPY requirements/ /snekbox/requirements/
+COPY --link requirements/ /snekbox/requirements/
WORKDIR /snekbox
-# pip installs to the default user site since PIP_USER is set.
RUN pip install -U -r requirements/requirements.pip
# This must come after the first pip command! From the docs:
@@ -58,11 +79,12 @@ ARG DEV
RUN if [ -n "${DEV}" ]; \
then \
pip install -U -r requirements/coverage.pip \
- && PYTHONUSERBASE=/snekbox/user_base pip install numpy~=1.19; \
+ && export PYTHONUSERBASE=/snekbox/user_base \
+ && /lang/python/default/bin/python -m pip install --user numpy~=1.19; \
fi
# At the end to avoid re-installing dependencies when only a config changes.
-COPY config/ /snekbox/config/
+COPY --link config/ /snekbox/config/
ENTRYPOINT ["gunicorn"]
CMD ["-c", "config/gunicorn.conf.py"]
diff --git a/NOTICE b/LICENSE-THIRD-PARTY
index b6e5fbc..684f2df 100644
--- a/NOTICE
+++ b/LICENSE-THIRD-PARTY
@@ -1,9 +1,36 @@
-The Python code at snekbox/config_pb2.py was generated from config.proto in nsjail
-Copyright 2014 Google Inc. All Rights Reserved.
-Copyright 2016 Sergiusz Bazanski. All Rights Reserved.
-
--------------------------------------------------------------------------------
-
+--------------------------------------------------------------------------------
+ MIT License
+Applies to:
+ - Copyright (c) 2014 Docker, Inc.
+ - scripts/build_python.sh: find command for de-bloating Python install
+--------------------------------------------------------------------------------
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--------------------------------------------------------------------------------
+ Apache License, Version 2.0
+Applies to:
+ - Copyright 2014 Google Inc. All Rights Reserved.
+ Copyright 2016 Sergiusz Bazanski. All Rights Reserved.
+ - snekbox/config_pb2.py: generated from config.proto in nsjail
+--------------------------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
diff --git a/README.md b/README.md
index 0b23848..6916090 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,7 @@
Python sandbox runners for executing code in isolation aka snekbox.
-Supports a memory [virtual read/write file system](#virtual-file-system) within the sandbox,
-allowing text or binary files to be sent and returned.
+Supports a memory [virtual read/write file system](#virtual-file-system) within the sandbox, allowing text or binary files to be sent and returned.
A client sends Python code to a snekbox, the snekbox executes the code, and finally the results of the execution are returned to the client.
@@ -100,22 +99,19 @@ Name | Description
## Third-party Packages
-By default, the Python interpreter has no access to any packages besides the
-standard library. Even snekbox's own dependencies like Falcon and Gunicorn are
-not exposed.
+By default, the Python interpreter has no access to any packages besides the standard library. Even snekbox's own dependencies like Falcon and Gunicorn are not exposed.
To expose third-party Python packages during evaluation, install them to a custom user site:
```sh
-docker exec snekbox /bin/sh -c 'PYTHONUSERBASE=/snekbox/user_base pip install numpy'
+docker exec snekbox /bin/sh -c \
+ 'PYTHONUSERBASE=/snekbox/user_base /lang/python/default/bin/python -m pip install --user numpy'
```
In the above command, `snekbox` is the name of the running container. The name may be different and can be checked with `docker ps`.
The packages will be installed to the user site within `/snekbox/user_base`. To persist the installed packages, a volume for the directory can be created with Docker. For an example, see [`docker-compose.yml`].
-If `pip`, `setuptools`, or `wheel` are dependencies or need to be exposed, then use the `--ignore-installed` option with pip. However, note that this will also re-install packages present in the custom user site, effectively making caching it futile. Current limitations of pip don't allow it to ignore packages extant outside the installation destination.
-
## Development Environment
See [CONTRIBUTING.md](.github/CONTRIBUTING.md).
diff --git a/config/snekbox.cfg b/config/snekbox.cfg
index 5dd63da..4e146ec 100644
--- a/config/snekbox.cfg
+++ b/config/snekbox.cfg
@@ -14,8 +14,10 @@ envar: "OPENBLAS_NUM_THREADS=5"
envar: "MKL_NUM_THREADS=5"
envar: "VECLIB_MAXIMUM_THREADS=5"
envar: "NUMEXPR_NUM_THREADS=5"
-envar: "PYTHONPATH=/snekbox/user_base/lib/python3.11/site-packages"
+envar: "PYTHONDONTWRITEBYTECODE=true"
envar: "PYTHONIOENCODING=utf-8:strict"
+envar: "PYTHONUNBUFFERED=true"
+envar: "PYTHONUSERBASE=/snekbox/user_base"
envar: "HOME=home"
keep_caps: false
@@ -79,29 +81,8 @@ mount {
}
mount {
- src: "/usr/local/lib"
- dst: "/usr/local/lib"
- is_bind: true
- rw: false
-}
-
-mount {
- src: "/usr/local/bin/python"
- dst: "/usr/local/bin/python"
- is_bind: true
- rw: false
-}
-
-mount {
- src: "/usr/local/bin/python3"
- dst: "/usr/local/bin/python3"
- is_bind: true
- rw: false
-}
-
-mount {
- src: "/usr/local/bin/python3.11"
- dst: "/usr/local/bin/python3.11"
+ src: "/lang"
+ dst: "/lang"
is_bind: true
rw: false
}
@@ -116,6 +97,6 @@ cgroup_pids_mount: "/sys/fs/cgroup/pids"
iface_no_lo: true
exec_bin {
- path: "/usr/local/bin/python"
- arg: "-BSqu"
+ path: "/lang/python/default/bin/python"
+ arg: ""
}
diff --git a/deployment.yaml b/deployment.yaml
index b0856f3..a6e7a31 100644
--- a/deployment.yaml
+++ b/deployment.yaml
@@ -30,8 +30,9 @@ spec:
- "/bin/sh"
- "-c"
- >-
- PYTHONUSERBASE=/snekbox/user_base
- pip install --user --upgrade
+ find /lang/python -mindepth 1 -maxdepth 1 -type d -exec
+ sh -c 'PYTHONUSERBASE=/snekbox/user_base &&
+ {}/bin/python -m pip install --user -U
anyio[trio]~=3.6
arrow~=1.2
attrs~=22.2
@@ -55,6 +56,7 @@ spec:
typing-extensions~=4.4
tzdata~=2022.7
yarl~=1.8
+ ' \;
volumes:
- name: snekbox-user-base-volume
hostPath:
diff --git a/scripts/build_python.sh b/scripts/build_python.sh
new file mode 100755
index 0000000..da937c2
--- /dev/null
+++ b/scripts/build_python.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+shopt -s inherit_errexit
+
+py_version="${1}"
+
+# Install Python interpreter under e.g. /lang/python/3.11/ (no patch version).
+"${PYENV_ROOT}/plugins/python-build/bin/python-build" \
+ "${py_version}" \
+ "/lang/python/${py_version%.*}"
+"/lang/python/${py_version%.*}/bin/python" -m pip install -U pip
+
+# Clean up some unnecessary files to reduce image size bloat.
+find /lang/python/ -depth \
+\( \
+ \( -type d -a \( \
+ -name test -o -name tests -o -name idle_test \
+ \) \) \
+ -o \( -type f -a \( \
+ -name '*.pyc' -o -name '*.pyo' -o -name 'libpython*.a' \
+ \) \) \
+\) -exec rm -rf '{}' +
diff --git a/snekbox/nsjail.py b/snekbox/nsjail.py
index f64830a..1de7b1e 100644
--- a/snekbox/nsjail.py
+++ b/snekbox/nsjail.py
@@ -221,9 +221,9 @@ class NsJail:
*nsjail_args,
"--",
self.config.exec_bin.path,
- *self.config.exec_bin.arg,
- # Filter out empty strings at start of py_args
+ # Filter out empty strings at start of Python args
# (causes issues with python cli)
+ *iter_lstrip(self.config.exec_bin.arg),
*iter_lstrip(py_args),
]
diff --git a/tests/test_nsjail.py b/tests/test_nsjail.py
index 5b06534..e422de5 100644
--- a/tests/test_nsjail.py
+++ b/tests/test_nsjail.py
@@ -79,7 +79,7 @@ class NsJailTests(unittest.TestCase):
for _ in range({max_pids}):
print(subprocess.Popen(
[
- '/usr/local/bin/python3',
+ '/lang/python/default/bin/python',
'-c',
'import time; time.sleep(1)'
],
@@ -486,7 +486,7 @@ class NsJailTests(unittest.TestCase):
for args, expected in cases:
with self.subTest(args=args):
result = self.nsjail.python3(py_args=args)
- idx = result.args.index("-BSqu")
+ idx = result.args.index(self.nsjail.config.exec_bin.path)
self.assertEqual(result.args[idx + 1 :], expected)
self.assertEqual(result.returncode, 0)