diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..bdbccde --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,230 @@ +name: main + +on: + schedule: + # update the pointers once a week + # https://crontab.guru/once-a-week + - cron: "0 0 * * 0" + push: + branches: main + pull_request: + branches: main + +jobs: + docker: + strategy: + fail-fast: false + matrix: + repository: + - 'ghcr.io' + - 'docker.io' + python: + - '3.12' + - '3.11' + - '3.10' + - '3.9' + - '3.8' + alpine: + - '3.20' + os: + - 'ubuntu-latest' + + runs-on: ${{ matrix.os }} + permissions: + packages: write + + steps: + - + name: Checkout + uses: actions/checkout@v4 + + - + name: Convert README.rst to markdown + uses: docker://pandoc/core:2.9 + with: + args: >- + -s + --wrap=none + -t gfm + -o README.md + README.rst + + - + name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + id: image_env + run: | + . ./env.sh \ + '${{ matrix.alpine }}' \ + '${{ matrix.python }}' \ + '${{ github.repository_owner }}' \ + '${{ matrix.repository }}' + + docker pull "${SOURCE_IMAGE}" + + echo ALPINE_VERSION="${ALPINE_VERSION}" >> "$GITHUB_OUTPUT" + echo PYTHON_VERSION="${PYTHON_VERSION}" >> "$GITHUB_OUTPUT" + echo SOURCE_IMAGE="${SOURCE_IMAGE}" >> "$GITHUB_OUTPUT" + echo IMAGE_TAG="${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo REPOSITORY="${REPOSITORY}" >> "$GITHUB_OUTPUT" + echo BASE_IMAGE_DIGEST="$(digest_of "$SOURCE_IMAGE")" >> "$GITHUB_OUTPUT" + echo 'LONG_DESCRIPTION<> "$GITHUB_OUTPUT" + > "$GITHUB_OUTPUT" + echo 'EOF' >> "$GITHUB_OUTPUT" + echo 'IMAGE_DESCRIPTION=Distroless Python '"$PYTHON_VERSION"' on alpine'"$ALPINE_VERSION"'. ${{ github.event.repository.description }}. See [${{ github.repository }}](https://github.com/${{ github.repository }}) for more info.' >> "$GITHUB_OUTPUT" + + - + name: Buildroot + uses: docker/build-push-action@v6 + with: + platforms: | + linux/amd64 + linux/arm64 + context: "." + file: Dockerfile.alpine + target: buildroot + cache-from: | + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot + type=registry,ref=docker.io/${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + build-args: | + ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }} + BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }} + SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }} + BUILD_ROOT=/d + tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot" + - + name: distroless + uses: docker/build-push-action@v6 + with: + platforms: | + linux/amd64 + linux/arm64 + context: "." + file: Dockerfile.alpine + # target: distroless-python + cache-from: | + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }} + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot + type=registry,ref=docker.io/${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + build-args: | + ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }} + BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }} + SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }} + BUILD_ROOT=/d + tags: "${{ steps.image_env.outputs.IMAGE_TAG }}" + # - + # name: distroless-tests + # uses: docker/build-push-action@v6 + # with: + # context: "." + # platforms: | + # linux/amd64 + # linux/arm64 + # file: Dockerfile.alpine + # target: tests + # cache-from: | + # type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }} + # type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot + # type=registry,ref=docker.io/${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + # build-args: | + # ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }} + # BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + # PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }} + # SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }} + # BUILD_ROOT=/d + # tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-test" + # - + # name: export annotations + # id: inspect + # run: | + # echo 'annotations<> "$GITHUB_OUTPUT" + # docker inspect '${{ steps.image_env.outputs.IMAGE_TAG }}' | jq -r '.[].Config.Labels| keys[] as $k | "\($k)=\(.[$k])"' >> "$GITHUB_OUTPUT" + # echo 'EOF' >> "$GITHUB_OUTPUT" + + - + name: Login to GitHub Container Registry + if: ${{ matrix.repository == 'ghcr.io' }} + uses: docker/login-action@v3 + with: + registry: 'ghcr.io' + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Login to DockerHub + if: ${{ matrix.repository == 'docker.io' }} + uses: docker/login-action@v3 + with: + registry: 'docker.io' + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - + name: Upload Buildroot + uses: docker/build-push-action@v6 + with: + push: true + platforms: | + linux/amd64 + linux/arm64 + context: "." + file: Dockerfile.alpine + target: buildroot + cache-from: | + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot + type=registry,ref=docker.io/${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + build-args: | + ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }} + BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }} + SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }} + BUILD_ROOT=/d + tags: "${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot" + - + name: Upload + uses: docker/build-push-action@v6 + env: + SOURCE_DATE_EPOCH: 0 + with: + push: true + context: "." + platforms: | + linux/amd64 + linux/arm64 + file: Dockerfile.alpine + cache-from: | + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }} + type=registry,ref=${{ steps.image_env.outputs.IMAGE_TAG }}-buildroot + type=registry,ref=docker.io/${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + build-args: | + ALPINE_VERSION=${{ steps.image_env.outputs.ALPINE_VERSION }} + BASE_IMAGE_DIGEST=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + PYTHON_VERSION=${{ steps.image_env.outputs.PYTHON_VERSION }} + SOURCE_IMAGE=${{ steps.image_env.outputs.SOURCE_IMAGE }} + BUILD_ROOT=/d + tags: "${{ steps.image_env.outputs.IMAGE_TAG }}" + labels: ${{steps.image_env.outputs.IMAGE_LABELS}} + sbom: true + annotations: | + index,manifest:org.opencontainers.image.authors=distroless-python image developers + index,manifest:org.opencontainers.image.source=https://github.com/autumnjolitz/distroless-python + index,manifest:org.opencontainers.image.title=distroless-python${{ steps.image_env.outputs.PYTHON_VERSION }}-alpine${{ steps.image_env.outputs.ALPINE_VERSION }} + index,manifest:org.opencontainers.image.description="${{ steps.image_env.outputs.IMAGE_DESCRIPTION }}" + index,manifest:org.opencontainers.image.base.digest=${{ steps.image_env.outputs.BASE_IMAGE_DIGEST }} + index,manifest:org.opencontainers.image.base.name=${{ steps.image_env.outputs.SOURCE_IMAGE }} + index,manifest:distroless.python-version=${{ steps.image_env.outputs.PYTHON_VERSION }} + index,manifest:distroless.alpine-version=${{ steps.image_env.outputs.ALPINE_VERSION }} + index,manifest:distroless.base-image=alpine${{ steps.image_env.outputs.ALPINE_VERSION }} + + - name: Update repo description + if: ${{ matrix.repository == 'docker.io' }} + uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4.0.0 + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + short-description: ${{ github.event.repository.description }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f98789e --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.egg-info/ +.idea/* +*.idx +*.sublime-* +*.pyc +dist/ +.cache/ +*.dat +.DS_Store +python/ +.pytest_cache/ +build/* +.pytype/* +instruct/about.py +python*/ +.coverage diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..31cb6a5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - + repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - + id: check-ast + - + id: check-case-conflict + - + id: check-executables-have-shebangs + - + id: check-merge-conflict + - + id: check-yaml + - + id: end-of-file-fixer + - + id: check-shebang-scripts-are-executable + - + id: detect-private-key + - + id: trailing-whitespace + args: + - '--markdown-linebreak-ext=rst' + - + repo: https://github.com/python/black + rev: '23.11.0' + hooks: + - + id: black + language_version: python3 diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000..d53043c --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,111 @@ +ARG ALPINE_VERSION=3.20 +ARG PYTHON_VERSION=3.12 +ARG SOURCE_IMAGE=docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} + +FROM --platform=$BUILDPLATFORM $SOURCE_IMAGE AS buildroot +ARG PYTHON_VERSION=3.12 + +ARG BUILD_ROOT='/dest' +ARG CACHE_ROOT='/cache' +ENV BUILD_ROOT=$BUILD_ROOT \ + CACHE_ROOT=$CACHE_ROOT \ + PYTHON_VERSION=$PYTHON_VERSION \ + _sys_apk_add="/usr/bin/env apk add --no-cache" \ + _apk_add="/usr/bin/env apk add --root $BUILD_ROOT --no-cache" \ + _apk_del="/usr/bin/env apk del --root $BUILD_ROOT --purge" \ + _sh="chroot $BUILD_ROOT sh" \ + _ln="chroot $BUILD_ROOT ln" \ + _chroot="chroot $BUILD_ROOT" + +RUN set -eu ; \ + python -m pip install -U pip setuptools ; \ + # Add to buildroot: + $_sys_apk_add \ + # dash is used as a /bin/sh replacement + dash \ + # TLS certs + ca-certificates \ + # zip is used to take all the bytecode compiled standard + # library and create a pythonXY.zip file that will + # be imported from. This makes the stdlib immutable. + zip \ + ; \ + # remove all ``__pycache__`` directories + find /usr/local/lib/python$PYTHON_VERSION -type d -name '__pycache__' -print0 | xargs -0 rm -rf ; \ + # compile all py to an adjacent pyc and remove the original, leaving only the bytecode + python -m compileall -b /usr/local/lib/python$PYTHON_VERSION ; \ + find -type f -name '*.py' -exec sh -c "[ -f \"{}c\" ] && echo 'Removing \"{}\"' && rm -f \"{}\"" \; ;\ + # make the new root: + mkdir -p \ + $CACHE_ROOT/ \ + $BUILD_ROOT/etc \ + $BUILD_ROOT/bin \ + $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/site-packages \ + $BUILD_ROOT/usr/local/bin \ + ; \ + # use a symlink to hold the apk related confs + ln -s /etc/apk $BUILD_ROOT/etc/apk ; \ + $_apk_add --initdb ; \ + $_apk_add \ + alpine-baselayout-data \ + alpine-release \ + musl \ + libffi \ + # needed for update-ca-certificates to work: + run-parts \ + # install the runtime dependencies for python + $(apk info -R .python-rundeps | grep -vE ':$') \ + ; \ + cp -p /bin/busybox $BUILD_ROOT/bin/busybox ; \ + ls -lt $BUILD_ROOT/bin/busybox ; \ + chroot $BUILD_ROOT /bin/busybox ln -sf /bin/busybox /bin/ln ; \ + # copy dash into the container so we can use it as the default bin/sh + tar -C / -cpf - $(\ + apk info -L \ + dash \ + ca-certificates \ + | grep -vE ':$' \ + ) | tar -C $BUILD_ROOT -xpf - ; \ + $_ln -sf /usr/bin/dash /bin/sh ; \ + (\ + cd /usr/local/lib && \ + tar -C /usr/local/lib -cpf - python$PYTHON_VERSION/lib-dynload libpython* | tar -C $BUILD_ROOT/usr/local/lib -xpf - ; \ + tar -C /usr/local/bin -cpf - python* | tar -C $BUILD_ROOT/usr/local/bin -xpf -; \ + (cd python$PYTHON_VERSION && zip -9 -X $BUILD_ROOT/usr/local/lib/python$(echo $PYTHON_VERSION | tr -d '.').zip $(\ + find . | grep -vE "(__pycache__|^\./(test|site-packages|lib-dynload|idlelib|lib2to3|tkinter|turtle|ensurepip|pydoc))" \ + )); \ + cp -p python$PYTHON_VERSION/os.pyc $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/os.pyc ; \ + touch $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/ensurepip.py ; \ + rm $BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/lib-dynload/_tkinter* ; \ + ) && \ + $_ln -sf /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python3 && \ + $_ln -sf /usr/local/bin/python$PYTHON_VERSION /usr/local/bin/python && \ + tar -C "$BUILD_ROOT" -cpf - etc/apk bin/ln bin/busybox var/cache/apk usr/share/apk | tar -C "$CACHE_ROOT" -xpf - ; \ + rm -rf $BUILD_ROOT/bin/ln $BUILD_ROOT/bin/busybox $BUILD_ROOT/etc/apk $BUILD_ROOT/var/cache/apk /usr/share/apk && \ + # regenerate the ca-certs! + chroot $BUILD_ROOT update-ca-certificates + + +FROM scratch AS distroless-python +ARG ALPINE_VERSION=3.20 +ARG PYTHON_VERSION=3.12 +ARG SOURCE_IMAGE=docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} +ARG BASE_IMAGE_DIGEST +ARG BUILD_ROOT='/dest' +ENV BUILD_ROOT=$BUILD_ROOT \ + PYTHON_VERSION=$PYTHON_VERSION \ + ALPINE_VERSION=$ALPINE_VERSION + +COPY --from=buildroot $BUILD_ROOT / +LABEL \ + org.opencontainers.image.authors="distroless-python image developers " \ + org.opencontainers.image.source="https://github.com/autumnjolitz/distroless-python" \ + org.opencontainers.image.title="Distroless Python ${PYTHON_VERSION} on alpine${ALPINE_VERSION}" \ + org.opencontainers.image.description="Distroless, optimized Python images distilled from the DockerHub official Python images. These images only have a python interpreter and the dash shell." \ + org.opencontainers.image.base.digest="${BASE_IMAGE_DIGEST}" \ + org.opencontainers.image.base.name="$SOURCE_IMAGE" \ + distroless.python-version="${PYTHON_VERSION}" \ + distroless.alpine-version="${ALPINE_VERSION}" \ + distroless.base-image="alpine${ALPINE_VERSION}" + +ENTRYPOINT [ "/usr/local/bin/python" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..41b98ed --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2024, Autumn Jolitz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the distroless-python project nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL DISTROLESS-PYTHON PROJECT CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..865b70b --- /dev/null +++ b/README.rst @@ -0,0 +1,144 @@ +distroless-python images +================================= + +.. list-table:: + :stub-columns: 1 + + * - Latest Build + - |github-actions| + * - Source + - ``_ + * - Issues + - ``_ + * - DockerHub + - ``_ + +Images +--------- + +DockerHub: + +.. code:: + + autumnjolitz/distroless-python:3.12-alpine3.20 + autumnjolitz/distroless-python:3.11-alpine3.20 + autumnjolitz/distroless-python:3.10-alpine3.20 + autumnjolitz/distroless-python:3.9-alpine3.20 + autumnjolitz/distroless-python:3.8-alpine3.20 + autumnjolitz/distroless-python:3.12-alpine3.20 + autumnjolitz/distroless-python:3.11-alpine3.20 + autumnjolitz/distroless-python:3.10-alpine3.20 + autumnjolitz/distroless-python:3.9-alpine3.20 + autumnjolitz/distroless-python:3.8-alpine3.20 + +Github Container Repository: + +.. code:: + + ghcr.io/autumnjolitz/distroless-python:3.12-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.11-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.10-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.9-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.8-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.12-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.11-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.10-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.9-alpine3.20 + ghcr.io/autumnjolitz/distroless-python:3.8-alpine3.20 + + + +About +------ + +A distroless image is one that has the **bare minimum** to run the application. + +By definition, a **distroless** image is **secure** as it has less code, less entrypoints. + +**distroless-python** builds off of the official `DockerHub python `_ images, which means that as the official images are updated, a refresh is a simple CI/CD run away to get any updates or bugfixes. + +.. code:: bash + + $ docker images autumnjolitz/distroless-python:3.12-alpine3.20 + REPOSITORY TAG IMAGE ID CREATED SIZE + autumnjolitz/distroless-python 3.12-alpine3.20 d34d43fd7a8a 30 minutes ago 27.8MB + + +a distroless-python image provides: + +* python3 +* dash +* ca-certificates (NB: Use ``update-ca-certificates`` to update them) + +To save space, the standard library has been byte-compiled and compressed into a zip file which is imported by the interpreter. + +ensurepip is replaced with a no-op to allow venv to continue functioning. + +Development +------------- + +For each image, there is a **-buildroot** companion package. You may ``FROM $SOURCE-buildroot AS builder`` in your own ``Dockerfile``s and add to the new root at ``$BUILD_ROOT``! + +The following is an example demonstrating the installation of a PyPI package (httpie) into a minimal image. + +Given the following ``Dockerfile``, we will add ``httpie`` to the image and reference just that! + +.. code:: dockerfile + + #syntax=docker/dockerfile:1 + FROM autumnjolitz/distroless-python:3.12-alpine3.20-buildroot AS buildroot + RUN python -m pip install \ + --no-cache \ + --prefix "$BUILD_ROOT/usr/local" \ + httpie + + FROM autumnjolitz/distroless-python:3.12-alpine3.20 + COPY --from=buildroot \ + /$BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/site-packages \ + /usr/local/lib/python$PYTHON_VERSION/site-packages + COPY --from=buildroot \ + /$BUILD_ROOT/usr/local/bin/http \ + /usr/local/bin/http + + ENTRYPOINT ["http"] + + +Build and test the image! + +.. code:: bash + + $ docker build -t httpie =f Dockerfile . + $ docker run --rm -it httpie pie.dev/get + HTTP/1.1 200 OK + Access-Control-Allow-Credentials: true + Access-Control-Allow-Origin: * + Connection: keep-alive + Content-Encoding: gzip + Content-Type: application/json + Date: Sat, 03 Aug 2024 07:00:04 GMT + Transfer-Encoding: chunked + alt-svc: h3=":443"; ma=86400 + + { + "args": {}, + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip", + "Connection": "Keep-Alive", + "Host": "pie.dev", + "User-Agent": "HTTPie/3.2.3" + }, + "origin": "[suppressed]", + "url": "http://pie.dev/get" + } + $ docker images test + REPOSITORY TAG IMAGE ID CREATED SIZE + httpie latest 7c6811df800d 3 minutes ago 43.3MB + + +Isn't that neat? Tiny images! + +Another example may be found at `examples/simple-flask/ `_! + +.. |github-actions| image:: https://github.com/autumnjolitz/distroless-python/actions/workflows/main.yml/badge.svg + :target: https://github.com/autumnjolitz/distroless-python/actions/workflows/main.yml diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..d4de6eb --- /dev/null +++ b/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env sh + +. env.sh + +log="$(mktemp image.log.XXXX)" +trap "rm -f $log" EXIT + +if ! >"$log" 2>&1 docker pull "$SOURCE_IMAGE"; then + >&2 echo 'Unable to find '"$SOURCE_IMAGE"'!' + >&2 cat "$log" + exit 4; +fi + + +BASE_IMAGE_DIGEST=$(digest_of "$SOURCE_IMAGE" "$log") +if [ "x$BASE_IMAGE_DIGEST" = 'x' ]; then + exit 88 +fi + +>&2 echo "BASE_IMAGE_DIGEST=${BASE_IMAGE_DIGEST}" + +if ! >"$log" 2>&1 docker build \ + --build-arg "ALPINE_VERSION=${ALPINE_VERSION}" \ + --build-arg "BASE_IMAGE_DIGEST=${BASE_IMAGE_DIGEST}" \ + --build-arg "PYTHON_VERSION=${PYTHON_VERSION}" \ + --build-arg "BUILD_ROOT=/d" \ + -f Dockerfile.alpine \ + -t "$IMAGE_TAG" \ + . ; then + >&2 echo 'Unable to build '"$IMAGE_TAG"'!' + >&2 cat "$log" + exit 8; +fi + +echo "$IMAGE_TAG" diff --git a/env.sh b/env.sh new file mode 100644 index 0000000..034f058 --- /dev/null +++ b/env.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env sh + +set -eu +set -o pipefail +shopt -s nullglob + +trim_slash() { + local s + for s in "${@:-}" + do + sed -E 's://*:/:g; s:(^/)?/*$:\1:' <<< "${s}" + done; +} + +ALPINE_VERSION="${1:-}" +PYTHON_VERSION="${2:-}" +ORG="${3:-}" + +if [ "x${ALPINE_VERSION}" = 'x' ]; then + >&2 echo 'missing ALPINE_VERSION' + exit 1 +fi + +if [ "x${PYTHON_VERSION}" = 'x' ]; then + >&2 echo 'missing PYTHON_VERSION' + exit 1 +fi + +REPOSITORY="${4:-}" + +if ! case $REPOSITORY in */) false ;; *) true ;; esac; then + # trim off the trailing slash + REPOSITORY="$(trim_slash "$REPOSITORY")" +fi + +if ! case $REPOSITORY in docker.io*) false ;; *) true ;; esac; then + # ARJ: The default context _is_ docker.io + REPOSITORY='' +fi + +IMAGE_TAG="distroless-python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}" +if [ "x$ORG" != 'x' ]; then + IMAGE_TAG="${ORG}/${IMAGE_TAG}" +fi + +if [ "x$REPOSITORY" != 'x' ]; then + IMAGE_TAG="${REPOSITORY}/${IMAGE_TAG}" +fi + +SOURCE_IMAGE="docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}" + +jq="$(command -v jq)" + +digest_of() { + local repo="${1:-}" + local log="${2:-}" + if [ "x$jq" = 'x' ]; then + >&2 echo 'jq not installed!' + return 101 + fi + if [ "x$repo" = 'x' ]; then + >&2 echo 'no repo provided!' + return 1 + fi + local workfile="$(mktemp digest_of.workfile.XXXX)" + if [ "x$log" = 'x' ]; then + log="$(mktemp digest_of.log.XXXX)" + trap "rm -f $log $workfile" EXIT + else + trap "rm -f $workfile" EXIT + fi + if ! 2>>"$log" docker inspect "$repo" | 2>>"$log" >"$workfile" $jq -r '.[].RepoDigests[]'; then + >&2 echo 'Unable to inspect '"$repo"'!' + >&2 cat "$log" + return 16 + fi + local digest=$(< "$workfile") + if [ "x$digest" = 'x' ]; then + >&2 echo 'digest empty?!' + return 88 + fi + echo "$digest" +} + +>&2 echo "ALPINE_VERSION=${ALPINE_VERSION}" +>&2 echo "PYTHON_VERSION=${PYTHON_VERSION}" +>&2 echo "SOURCE_IMAGE=${SOURCE_IMAGE}" +>&2 echo "IMAGE_TAG=${IMAGE_TAG}" +>&2 echo "REPOSITORY=${REPOSITORY}" + +export ALPINE_VERSION +export PYTHON_VERSION +export SOURCE_IMAGE +export IMAGE_TAG +export REPOSITORY diff --git a/examples/simple-flask/Dockerfile b/examples/simple-flask/Dockerfile new file mode 100644 index 0000000..0bd3279 --- /dev/null +++ b/examples/simple-flask/Dockerfile @@ -0,0 +1,25 @@ +FROM autumnjolitz/distroless-python:3.12-alpine3.20-buildroot AS buildroot + +ADD requirements.txt . + +RUN python -m pip install \ + --no-cache \ + --prefix "$BUILD_ROOT/usr/local" \ + -r requirements.txt + +FROM autumnjolitz/distroless-python:3.12-alpine3.20 as runtime +EXPOSE 8080 + +COPY --from=buildroot \ + /$BUILD_ROOT/usr/local/lib/python$PYTHON_VERSION/site-packages \ + /usr/local/lib/python$PYTHON_VERSION/site-packages +COPY --from=buildroot \ + /$BUILD_ROOT/usr/local/bin/flask \ + /usr/local/bin/flask + +WORKDIR /app +ADD hello.py . +ENV FLASK_RUN_PORT=8080 + +ENTRYPOINT ["flask"] +CMD ["--app", "hello", "run", "--host=0.0.0.0"] diff --git a/examples/simple-flask/README.rst b/examples/simple-flask/README.rst new file mode 100644 index 0000000..27d884d --- /dev/null +++ b/examples/simple-flask/README.rst @@ -0,0 +1,24 @@ +=================== +simple flask app +=================== + +Install Flask using pip. Run in safe pip-less environment. + +.. code:: bash + + $ cd examples/simple-flask/ + $ 2>/dev/null docker build -t hello . + $ docker images hello + REPOSITORY TAG IMAGE ID CREATED SIZE + hello latest d5108767e49e 15 seconds ago 32.4MB + $ docker run -p8080:8080 --rm -it hello + * Serving Flask app 'hello' + * Debug mode: off + WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:8080 + * Running on http://172.17.0.2:8080 + Press CTRL+C to quit + 192.168.65.1 - - [03/Aug/2024 06:37:49] "GET / HTTP/1.1" 200 - + ^C + $ diff --git a/examples/simple-flask/hello.py b/examples/simple-flask/hello.py new file mode 100644 index 0000000..0d2c13a --- /dev/null +++ b/examples/simple-flask/hello.py @@ -0,0 +1,8 @@ +from flask import Flask + +app = Flask(__name__) + + +@app.route("/") +def hello_world() -> str: + return "

Hello, World!

" diff --git a/examples/simple-flask/requirements.txt b/examples/simple-flask/requirements.txt new file mode 100644 index 0000000..e3e9a71 --- /dev/null +++ b/examples/simple-flask/requirements.txt @@ -0,0 +1 @@ +Flask