Skip to content

Commit

Permalink
feat: introducing distroless-python
Browse files Browse the repository at this point in the history
  • Loading branch information
autumnjolitz committed Aug 3, 2024
0 parents commit d114351
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 0 deletions.
95 changes: 95 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: main

on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]

jobs:
docker:
strategy:
fail-fast: false
matrix:
experimental: [false]
arch:
- 'x64'
python:
- '3.12'
alpine:
- '3.20'
os:
- 'ubuntu-latest'

runs-on: ${{ matrix.os }}

steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
id: image_env
run: |
. ./env.sh '${{ matrix.alpine }}' '${{ matrix.python }}' '${{ github.repository_owner }}'
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 REPO_TAG="${REPO_TAG}" >> "$GITHUB_OUTPUT"
echo BASE_IMAGE_DIGEST="$(digest_of "$SOURCE_IMAGE")" >> "$GITHUB_OUTPUT"
-
name: Build deps
uses: docker/build-push-action@v6
with:
push: false
pull: false
load: true
context: "."
file: Dockerfile.alpine
target: buildroot
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.REPO_TAG }}-buildroot
cache-to: |
type=registry,ref=${{ steps.image_env.outputs.REPO_TAG }}-buildroot,mode=max
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.REPO_TAG }}-buildroot"
-
name: Build
uses: docker/build-push-action@v6
with:
push: false
pull: false
load: true
context: "."
file: Dockerfile.alpine
cache-from: |
type=registry,ref=${{ steps.image_env.outputs.REPO_TAG }}
type=registry,ref=${{ steps.image_env.outputs.REPO_TAG }}-buildroot
type=registry,ref=${{ steps.image_env.outputs.SOURCE_IMAGE }}
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.REPO_TAG }}"
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
96 changes: 96 additions & 0 deletions Dockerfile.alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
ARG ALPINE_VERSION=3.20
ARG PYTHON_VERSION=3.12

FROM docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION} AS buildroot
ARG PYTHON_VERSION=3.12

ARG BUILD_ROOT='/dest'
ENV BUILD_ROOT=$BUILD_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"

RUN set -eu ; \
# Add to buildroot:
$_sys_apk_add \
# dash is used as a /bin/sh replacement
dash \
# 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 \
$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 \
# 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 \
| 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 && \
rm $BUILD_ROOT/bin/ln $BUILD_ROOT/bin/busybox $BUILD_ROOT/etc/apk && \
rm -rf $BUILD_ROOT/var/cache/apk /usr/share/apk



FROM scratch AS distroless-python
ARG ALPINE_VERSION=3.20
ARG PYTHON_VERSION=3.12
ARG BASE_IMAGE_DIGEST
ARG BUILD_ROOT='/dest'
ENV BUILD_ROOT=$BUILD_ROOT

COPY --from=buildroot $BUILD_ROOT /
LABEL \
org.opencontainers.image.authors="distroless-python image developers <[email protected]>" \
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="docker.io/python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}" \
distroless.python-version="${PYTHON_VERSION}" \
distroless.alpine-version="${ALPINE_VERSION}" \
distroless.base-image="alpine${ALPINE_VERSION}"

ENTRYPOINT [ "/usr/local/bin/python" ]
24 changes: 24 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env sh
set -x

. env.sh
set +x

log="$(mktemp image.log.XXXX)"
trap "rm -f $log $workfile" 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")
if [ "x$BASE_IMAGE_DIGEST" = 'x' ]; then
exit 88
fi

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 "$REPO_TAG" \
. ; then
>&2 echo 'Unable to build '"$REPO_TAG"'!'
>&2 cat "$log"
exit 8;
fi

echo "$REPO_TAG"
66 changes: 66 additions & 0 deletions env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env sh

set -eu
set -o pipefail
shopt -s nullglob


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

REPO_TAG="distroless-python:${PYTHON_VERSION}-alpine${ALPINE_VERSION}"
if [ "x$ORG" != 'x' ]; then
REPO_TAG="${ORG}/${REPO_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"
}


export ALPINE_VERSION
export PYTHON_VERSION
export SOURCE_IMAGE
export REPO_TAG

0 comments on commit d114351

Please sign in to comment.