diff options
author | 2023-08-29 19:35:53 +0100 | |
---|---|---|
committer | 2023-08-29 19:35:53 +0100 | |
commit | f85116ab91229d3f45a8292978ffd2af7821c70e (patch) | |
tree | ff08fbd7ee351092948eca109b320d99f05338a1 | |
parent | Merge pull request #183 from python-discord/enforce-filesize-limits (diff) | |
parent | Install 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-- | .dockerignore | 2 | ||||
-rw-r--r-- | .github/CONTRIBUTING.md | 8 | ||||
-rw-r--r-- | Dockerfile | 82 | ||||
-rw-r--r-- | LICENSE-THIRD-PARTY (renamed from NOTICE) | 39 | ||||
-rw-r--r-- | README.md | 12 | ||||
-rw-r--r-- | config/snekbox.cfg | 33 | ||||
-rw-r--r-- | deployment.yaml | 6 | ||||
-rwxr-xr-x | scripts/build_python.sh | 22 | ||||
-rw-r--r-- | snekbox/nsjail.py | 4 | ||||
-rw-r--r-- | tests/test_nsjail.py | 4 |
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/ @@ -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/ @@ -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) |