Repository navigation
feat: add a minimal runtime image #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4ddd380
4652fdc
b5a9ce3
62fbec7
e107f94
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| name: Runtime | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main] | ||
| paths: | ||
| - Dockerfile.runtime | ||
| - .github/workflows/runtime.yml | ||
| - example/** | ||
| - tests/runtime.py | ||
| pull_request: | ||
| paths: | ||
| - Dockerfile.runtime | ||
| - .github/workflows/runtime.yml | ||
| - example/** | ||
| - tests/runtime.py | ||
|
|
||
| concurrency: | ||
| group: runtime-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| test: | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - platform: linux/amd64 | ||
| architecture: amd64 | ||
| runner: ubuntu-latest | ||
| - platform: linux/arm64 | ||
| architecture: arm64 | ||
| runner: ubuntu-24.04-arm | ||
| runs-on: ${{ matrix.runner }} | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| - name: Build runtime and example | ||
| env: | ||
| PLATFORM: ${{ matrix.platform }} | ||
| run: | | ||
| docker build --platform="$PLATFORM" -f Dockerfile.runtime -t conda-runtime:test \ | ||
| --label "org.opencontainers.image.source=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \ | ||
| --label "org.opencontainers.image.revision=$GITHUB_SHA" . | ||
| docker build --platform="$PLATFORM" --build-arg RUNTIME_IMAGE=conda-runtime:test \ | ||
| -f example/Dockerfile -t conda-runtime:example example | ||
| - name: Test copied environment and provenance | ||
| run: | | ||
| docker run --rm --read-only --tmpfs /tmp --user=65532:65532 \ | ||
| --mount "type=bind,source=$PWD/tests/runtime.py,target=/runtime.py,readonly" \ | ||
| conda-runtime:example python /runtime.py | ||
| - name: Export tested runtime | ||
| env: | ||
| ARCHITECTURE: ${{ matrix.architecture }} | ||
| run: docker save --output "runtime-$ARCHITECTURE.tar" conda-runtime:test | ||
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | ||
| with: | ||
| name: runtime-${{ matrix.architecture }} | ||
| path: runtime-${{ matrix.architecture }}.tar | ||
| if-no-files-found: error | ||
|
|
||
| publish: | ||
| needs: test | ||
| if: github.event_name == 'push' && github.ref_name == 'main' | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| packages: write | ||
| steps: | ||
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | ||
| with: | ||
| pattern: runtime-* | ||
| path: images | ||
| merge-multiple: true | ||
| - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 | ||
| - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
| - name: Publish tested platform images and manifest | ||
| env: | ||
| IMAGE: ghcr.io/prefix-dev/conda-runtime | ||
| run: | | ||
| for architecture in amd64 arm64; do | ||
| docker load --input "images/runtime-$architecture.tar" | ||
| tag="$IMAGE:glibc-bash-$GITHUB_SHA-$architecture" | ||
| docker tag conda-runtime:test "$tag" | ||
| docker push "$tag" | ||
| done | ||
| docker buildx imagetools create \ | ||
| --tag "$IMAGE:glibc-bash-$GITHUB_SHA" \ | ||
| --tag "$IMAGE:glibc-bash" \ | ||
| "$IMAGE:glibc-bash-$GITHUB_SHA-amd64" \ | ||
| "$IMAGE:glibc-bash-$GITHUB_SHA-arm64" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| FROM cgr.dev/chainguard/wolfi-base@sha256:918a593b8268c222afd4e2c4f06860ac984e60719b4697e4c71d796bc8fcd042 AS source | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do they also have tags that dependabot can update next to the digest? this currently is hard to understand from which version it's coming from |
||
|
|
||
| RUN apk add --no-cache \ | ||
| bash=5.3-r13 \ | ||
| ncurses=6.6.20260905-r0 \ | ||
| ncurses-terminfo-base=6.6.20260905-r0 | ||
|
Comment on lines
+4
to
+6
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how are we planning on updating those? |
||
|
|
||
| # Extract runtime files and retain the source package SBOM for each component. | ||
| # The SBOMs describe upstream packages, not complete packages installed here. | ||
| RUN set -eu; \ | ||
| mkdir -p /rootfs/bin /rootfs/lib /rootfs/lib64 /rootfs/usr/lib /rootfs/etc /rootfs/tmp \ | ||
| /rootfs/usr/share/conda-runtime/sbom; \ | ||
| chmod 1777 /rootfs/tmp; \ | ||
| copy_runtime_file() { \ | ||
| cp -L "$1" "$2"; \ | ||
| package="$(apk info --who-owns "$(readlink -f "$1")")"; \ | ||
| package="${package##* }"; \ | ||
| cp "/var/lib/db/sbom/${package}.spdx.json" /rootfs/usr/share/conda-runtime/sbom/; \ | ||
| printf '%s\t%s\t%s\n' "${2#/rootfs}" "$1" "$package" \ | ||
| >> /rootfs/usr/share/conda-runtime/origins.tsv; \ | ||
| }; \ | ||
| apk --repositories-file /dev/null info -L glibc ld-linux > /tmp/glibc-files; \ | ||
| while IFS= read -r file; do \ | ||
| case "$file" in \ | ||
| usr/lib/*/*.so*) ;; \ | ||
| usr/lib/*.so*) copy_runtime_file "/$file" "/rootfs/lib/${file##*/}" ;; \ | ||
| esac; \ | ||
| done < /tmp/glibc-files; \ | ||
| for loader in /rootfs/lib/ld-linux-*.so.*; do \ | ||
| test -f "$loader"; \ | ||
| ln -s "/lib/${loader##*/}" "/rootfs/lib64/${loader##*/}"; \ | ||
| done; \ | ||
| find /usr/lib/locale -type f > /tmp/locale-files; \ | ||
| while IFS= read -r file; do \ | ||
| mkdir -p "/rootfs${file%/*}"; \ | ||
| copy_runtime_file "$file" "/rootfs$file"; \ | ||
| done < /tmp/locale-files; \ | ||
| for file in passwd group nsswitch.conf; do \ | ||
| copy_runtime_file "/etc/$file" "/rootfs/etc/$file"; \ | ||
| done; \ | ||
| copy_runtime_file /usr/bin/bash /rootfs/bin/bash; \ | ||
| copy_runtime_file /usr/lib/libtinfo.so.6 /rootfs/lib/libtinfo.so.6; \ | ||
| cd /rootfs; \ | ||
| find bin lib usr/lib etc -type f -exec sha256sum {} + > /tmp/runtime-checksums; \ | ||
| LC_ALL=C sort /tmp/runtime-checksums > usr/share/conda-runtime/files.sha256 | ||
|
|
||
| FROM scratch | ||
| COPY --from=source /rootfs/ / | ||
| ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin | ||
| ENV LANG=C.UTF-8 | ||
| CMD ["/bin/bash"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| """Exercise a copied Pixi environment and verify its base image provenance.""" | ||
|
|
||
| import ctypes | ||
| import grp | ||
| import hashlib | ||
| import json | ||
| import locale | ||
| import os | ||
| from pathlib import Path | ||
| import pwd | ||
| import sqlite3 | ||
| import subprocess | ||
| import tempfile | ||
| import time | ||
| import urllib.error | ||
| import urllib.request | ||
|
|
||
|
|
||
| assert os.getpid() == 1, "The shell hook must exec the application" | ||
| pwd.getpwuid(os.getuid()) | ||
| grp.getgrgid(os.getgid()) | ||
| locale.setlocale(locale.LC_ALL, "") | ||
| assert locale.getencoding().lower().replace("-", "") == "utf8" | ||
| with tempfile.TemporaryFile() as temporary_file: | ||
| temporary_file.write(b"runtime") | ||
| with sqlite3.connect(":memory:") as connection: | ||
| assert connection.execute("SELECT 6 * 7").fetchone() == (42,) | ||
| with urllib.request.urlopen("https://prefix.dev", timeout=30) as response: | ||
| assert response.status == 200 | ||
|
|
||
| # Run pthread_exit entirely in native code, without unwinding a Python callback. | ||
| libc = ctypes.CDLL("libc.so.6") | ||
| thread = ctypes.c_ulong() | ||
| libc.pthread_create.argtypes = [ | ||
| ctypes.POINTER(ctypes.c_ulong), | ||
| ctypes.c_void_p, | ||
| ctypes.c_void_p, | ||
| ctypes.c_void_p, | ||
| ] | ||
| libc.pthread_create.restype = ctypes.c_int | ||
| libc.pthread_join.argtypes = [ctypes.c_ulong, ctypes.POINTER(ctypes.c_void_p)] | ||
| libc.pthread_join.restype = ctypes.c_int | ||
| assert libc.pthread_create( | ||
| ctypes.byref(thread), None, ctypes.cast(libc.pthread_exit, ctypes.c_void_p), None | ||
| ) == 0 | ||
| assert libc.pthread_join(thread, None) == 0 | ||
|
|
||
| metadata = Path("/usr/share/conda-runtime") | ||
| checksums = {} | ||
| for line in (metadata / "files.sha256").read_text().splitlines(): | ||
| checksum, filename = line.split(" ", 1) | ||
| path = Path("/") / filename | ||
| assert hashlib.sha256(path.read_bytes()).hexdigest() == checksum, path | ||
| checksums[str(path)] = checksum | ||
|
|
||
| origins = {} | ||
| for line in (metadata / "origins.tsv").read_text().splitlines(): | ||
| destination, source, package = line.split("\t") | ||
| origins[destination] = source | ||
| sbom = json.loads((metadata / "sbom" / f"{package}.spdx.json").read_text()) | ||
| assert any( | ||
| f"{component['name']}-{component.get('versionInfo', '')}" == package | ||
| for component in sbom["packages"] | ||
| ), package | ||
| assert origins.keys() == checksums.keys(), "Every extracted file needs provenance" | ||
|
|
||
| server = subprocess.Popen( | ||
| ["gunicorn", "-w", "1", "docker_project:app", "--bind", "127.0.0.1:8000"] | ||
| ) | ||
| try: | ||
| deadline = time.monotonic() + 30 | ||
| while time.monotonic() < deadline: | ||
| assert server.poll() is None, "Gunicorn exited before serving a request" | ||
| try: | ||
| with urllib.request.urlopen("http://127.0.0.1:8000", timeout=5) as response: | ||
| assert response.read() == b"Hello, Pixi server!" | ||
| break | ||
| except (urllib.error.URLError, TimeoutError): | ||
| time.sleep(0.1) | ||
| else: | ||
| raise RuntimeError("Gunicorn did not become ready") | ||
| finally: | ||
| server.terminate() | ||
| server.wait(timeout=10) | ||
| assert server.returncode == 0, "Gunicorn must shut down cleanly on SIGTERM" | ||
| print("Runtime, HTTPS, HTTP server, and file/SBOM provenance checks passed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.