diff --git a/.dockerignore b/.dockerignore index f0f644e..85a2738 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,5 @@ +.git +.github +tests LICENSE README.md diff --git a/.github/workflows/anchore-analysis.yml b/.github/workflows/anchore-analysis.yml index bd186e3..08cb759 100644 --- a/.github/workflows/anchore-analysis.yml +++ b/.github/workflows/anchore-analysis.yml @@ -16,20 +16,34 @@ on: - cron: '24 5 * * 4' workflow_dispatch: +permissions: + contents: read + security-events: write + jobs: Anchore-Build-Scan: runs-on: ubuntu-latest steps: - name: Checkout the code uses: actions/checkout@v7 + - name: Lint shell scripts + run: | + shellcheck -x bin/bootstrap.sh lib/common.sh tests/smoke.sh + shellcheck -s bash -S error bashrc + - name: Lint Dockerfile + run: docker run --rm -i hadolint/hadolint < Dockerfile - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: build local container - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: tags: localbuild/testimage:latest push: false load: true + # The Dependabot auto-merge workflow waits on this workflow, so a failing + # smoke test also blocks automatic merges. + - name: Smoke test the image + run: docker run --rm -v "$PWD/tests:/tests:ro" localbuild/testimage:latest bash /tests/smoke.sh - name: Scan image id: scan uses: anchore/scan-action@v7 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1bb8187..f387305 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,8 +9,11 @@ on: workflow_dispatch: env: - # TODO: Change variable to your image's name. IMAGE_NAME: kjake/base + PLATFORMS: linux/386,linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x,linux/riscv64 + +permissions: + contents: read jobs: push: @@ -23,14 +26,30 @@ jobs: uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 + # Build and test the native image before anything is published, so a + # broken rebuild never replaces the image that child containers use. + - name: Build test image + uses: docker/build-push-action@v7 + with: + tags: localbuild/testimage:latest + push: false + load: true + - name: Smoke test the image + run: docker run --rm -v "$PWD/tests:/tests:ro" localbuild/testimage:latest bash /tests/smoke.sh + - name: Compute date tag + id: date + run: echo "tag=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - name: Login to Docker Hub uses: docker/login-action@v4.6.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v7 with: - platforms: linux/386,linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x,linux/riscv64 - tags: ${{ env.IMAGE_NAME }}:latest - push: true \ No newline at end of file + platforms: ${{ env.PLATFORMS }} + # A dated tag lets child images pin a known-good snapshot of testing. + tags: | + ${{ env.IMAGE_NAME }}:latest + ${{ env.IMAGE_NAME }}:${{ steps.date.outputs.tag }} + push: true diff --git a/Dockerfile b/Dockerfile index 1b77513..360d715 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,26 @@ FROM debian:testing-slim -LABEL maintainer="kjake" +LABEL maintainer="kjake" \ + org.opencontainers.image.title="kjake/base" \ + org.opencontainers.image.description="Debian testing base image with contrib and non-free enabled" \ + org.opencontainers.image.source="https://github.com/kjake/docker-base" \ + org.opencontainers.image.licenses="GPL-3.0" ENV HOME=/root ENV DEBIAN_FRONTEND=noninteractive -# Configure Apt -ADD sources.list /etc/apt/sources.list +# Configure Apt. This replaces the base image's deb822 source list; see the +# comments in debian.sources for why it starts out on http. +COPY debian.sources /etc/apt/sources.list.d/debian.sources -# Prepare environment -ADD lib/common.sh /app/lib/common.sh -ADD bin/bootstrap.sh /app/bin/bootstrap.sh -RUN chmod 0755 /app/bin/bootstrap.sh +# Prepare environment. /app/lib/common.sh is kept for downstream images; +# bootstrap.sh removes itself once it has run. +COPY lib/common.sh /app/lib/common.sh +COPY --chmod=0755 bin/bootstrap.sh /app/bin/bootstrap.sh RUN /app/bin/bootstrap.sh # Install Chambana.net bashrc -ADD bashrc /etc/bash.bashrc +COPY bashrc /etc/bash.bashrc ENV LC_ALL=C.UTF-8 ENV TERM=xterm diff --git a/README.md b/README.md index c7c778c..956aafc 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,49 @@ # docker-base + My base docker image, forked from chambana-net/docker-base, based on debian. + +The image is built from `debian:testing-slim` so that containers built on top of +it get newer community-maintained package releases than Debian stable offers. +It is published to Docker Hub as `kjake/base` for 386, amd64, arm64, arm/v7, +ppc64le, s390x and riscv64. + +## Tags + +| Tag | Meaning | +|--------------|-----------------------------------------------------------| +| `latest` | Most recent weekly rebuild of Debian testing. | +| `YYYYMMDD` | The rebuild from that day; pin this for reproducible builds. | + +## What the image provides + +- Apt sources for `testing`, `testing-updates` and `testing-security`, over + https, with the `main`, `contrib`, `non-free` and `non-free-firmware` + components enabled. See `debian.sources`. +- A fully upgraded package set plus `ca-certificates`, `locales`, `less`, + `patch`, `diffutils`, `debconf-utils` and `vim-tiny` linked as `vim`. +- `DEBIAN_FRONTEND=noninteractive`, `LC_ALL=C.UTF-8` and `TERM=xterm`. +- `/app/lib/common.sh`, a small helper library for child images with `MSG`, + `ERR`, `CHECK_BIN` and `CHECK_VAR`. +- An interactive `/etc/bash.bashrc` with the Chambana prompt. + +## Building and testing + +```sh +docker build -t kjake/base . +docker run --rm -v "$PWD/tests:/tests:ro" kjake/base bash /tests/smoke.sh +``` + +The build fails if any apt repository cannot be fetched. `tests/smoke.sh` +checks the apt configuration, installed tooling, environment and helper +library; set `SMOKE_OFFLINE=1` to skip the checks that need network access. + +## CI + +- **Docker**: weekly and on every push to `master`, builds and smoke tests + the image, then builds all platforms and pushes `latest` and a dated tag. +- **Anchore Container Scan**: on pull requests, weekly and on push; lints the + scripts and Dockerfile, builds and smoke tests the image, then scans it with + Grype and uploads the results to code scanning. +- **Dependabot auto-merge**: merges Dependabot pull requests once the scan + workflow succeeds. +- **Keepalive**: keeps scheduled workflows from being disabled by inactivity. diff --git a/bashrc b/bashrc index be06be1..7587f94 100755 --- a/bashrc +++ b/bashrc @@ -186,17 +186,18 @@ if [ -f /etc/bash_completion ] && ! shopt -oq posix; then fi # if the command-not-found package is installed, use it -if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found ]; then +if [ -x /usr/lib/command-not-found ] || [ -x /usr/share/command-not-found/command-not-found ]; then function command_not_found_handle { - # check because c-n-f could've been removed in the meantime - if [ -x /usr/lib/command-not-found ]; then - /usr/bin/python /usr/lib/command-not-found -- $1 - return $? - elif [ -x /usr/share/command-not-found ]; then - /usr/bin/python /usr/share/command-not-found -- $1 - return $? + # check because c-n-f could've been removed in the meantime + if [ -x /usr/lib/command-not-found ]; then + /usr/lib/command-not-found -- "$1" + return $? + elif [ -x /usr/share/command-not-found/command-not-found ]; then + /usr/share/command-not-found/command-not-found -- "$1" + return $? else - return 127 + printf "%s: command not found\n" "$1" >&2 + return 127 fi } fi diff --git a/bin/bootstrap.sh b/bin/bootstrap.sh index 27f9020..b1bf94f 100755 --- a/bin/bootstrap.sh +++ b/bin/bootstrap.sh @@ -3,12 +3,22 @@ # Preparation script based on https://github.com/olberger/baseimage-docker # +set -o errexit -o pipefail + +# shellcheck source=lib/common.sh disable=SC1091 . /app/lib/common.sh APT_INSTALL='apt-get install -y --no-install-recommends' +SOURCES=/etc/apt/sources.list.d/debian.sources + +## Fail the build if any configured repository cannot be fetched, instead of +## silently continuing with missing or stale package lists. +apt_update() { + apt-get -qq update --error-on=any +} MSG "Updating apt repositories..." -apt-get -qq update +apt_update ## Temporarily disable dpkg fsync to make building faster. echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/02apt-speedup @@ -29,7 +39,12 @@ ln -sf /bin/true /usr/bin/ischroot MSG "Installing packages..." $APT_INSTALL apt-utils -$APT_INSTALL ca-certificates apt-transport-https diffutils patch locales debconf-utils vim-tiny less +$APT_INSTALL ca-certificates diffutils patch locales debconf-utils vim-tiny less + +## Now that ca-certificates is present, fetch packages over https. +MSG "Switching apt repositories to https..." +sed -i 's|http://deb.debian.org/|https://deb.debian.org/|' "$SOURCES" +apt_update ## Link vim -> vim.tiny ln -sf /usr/bin/vim.tiny /usr/bin/vim diff --git a/debian.sources b/debian.sources new file mode 100644 index 0000000..eed69dc --- /dev/null +++ b/debian.sources @@ -0,0 +1,20 @@ +# Debian testing ("rolling") with every archive component enabled, so images +# built on top of this one get newer upstream releases than stable offers and +# can install contrib and non-free packages. +# +# Plain http is only used for the very first `apt-get update`, because the +# slim base image ships without ca-certificates. bin/bootstrap.sh installs +# ca-certificates and then switches these URIs to https. Packages are verified +# with the archive signing key either way. + +Types: deb +URIs: http://deb.debian.org/debian +Suites: testing testing-updates +Components: main contrib non-free non-free-firmware +Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp + +Types: deb +URIs: http://deb.debian.org/debian-security +Suites: testing-security +Components: main contrib non-free non-free-firmware +Signed-By: /usr/share/keyrings/debian-archive-keyring.pgp diff --git a/sources.list b/sources.list deleted file mode 100644 index 5878924..0000000 --- a/sources.list +++ /dev/null @@ -1,4 +0,0 @@ -deb https://cdn-fastly.deb.debian.org/debian/ testing main contrib non-free non-free-firmware -deb https://cdn-fastly.deb.debian.org/debian/ testing-updates main contrib non-free non-free-firmware -deb https://security.debian.org/debian-security testing-security/updates main contrib non-free non-free-firmware -deb https://cdn-fastly.deb.debian.org/debian/ testing-backports main contrib non-free non-free-firmware \ No newline at end of file diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..2fd662c --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Several checks deliberately pass single-quoted scripts to an inner bash. +# shellcheck disable=SC2016 +# +# Smoke tests for the kjake/base image. Run inside a container built from it: +# +# docker run --rm -v "$PWD/tests:/tests:ro" kjake/base bash /tests/smoke.sh +# +# Set SMOKE_OFFLINE=1 to skip the checks that need to reach the Debian mirrors. + +set -o nounset -o pipefail + +failures=0 + +pass() { printf 'ok - %s\n' "$1"; } +fail() { printf 'FAIL - %s\n' "$1"; failures=$((failures + 1)); } + +# check "description" command [args...] +check() { + local desc=$1 + shift + if "$@" >/tmp/smoke.out 2>&1; then + pass "$desc" + else + fail "$desc" + sed 's/^/ /' /tmp/smoke.out + fi +} + +SOURCES=/etc/apt/sources.list.d/debian.sources + +## Apt configuration +check "deb822 source list is present" test -f "$SOURCES" +check "no legacy /etc/apt/sources.list" test ! -e /etc/apt/sources.list +check "sources use https" grep -q '^URIs: https://deb.debian.org/debian$' "$SOURCES" +check "sources track testing" grep -q '^Suites: testing testing-updates$' "$SOURCES" +check "sources include testing-security" grep -q '^Suites: testing-security$' "$SOURCES" +check "no plain http sources remain" bash -c "! grep -q 'http://' '$SOURCES'" +check "contrib and non-free components enabled" \ + bash -c "[ \"\$(grep -c '^Components: main contrib non-free non-free-firmware$' '$SOURCES')\" -eq 2 ]" +check "apt package lists were cleaned" bash -c '[ -z "$(ls -A /var/lib/apt/lists | grep -v -e ^lock$ -e ^partial$ -e ^auxfiles$)" ]' +check "build-time dpkg speedup removed" test ! -e /etc/dpkg/dpkg.cfg.d/02apt-speedup + +if [ "${SMOKE_OFFLINE:-0}" != 1 ]; then + check "apt-get update succeeds for every repository" apt-get -qq update --error-on=any + for component in main contrib non-free non-free-firmware; do + check "testing/$component index is available" bash -c "apt-cache policy | grep -q ' testing/$component '" + done + # testing-security usually carries no packages, so it publishes no indexes; + # check that its signed release file was fetched instead. + check "testing-security release file was fetched" \ + bash -c 'ls /var/lib/apt/lists/*debian-security_dists_testing-security_InRelease' + rm -rf /var/lib/apt/lists/* +fi + +## Installed tooling +for pkg in apt-utils ca-certificates diffutils patch locales debconf-utils vim-tiny less; do + check "package $pkg installed" bash -c "dpkg-query -W -f='\${Status}' $pkg | grep -q 'install ok installed'" +done +check "vim points at vim.tiny" test "$(readlink /usr/bin/vim)" = /usr/bin/vim.tiny +check "ischroot is diverted to true" /usr/bin/ischroot +check "INITRD disabled for container" test "$(cat /etc/container_environment/INITRD)" = no + +## Environment +check "DEBIAN_FRONTEND is noninteractive" test "${DEBIAN_FRONTEND:-}" = noninteractive +check "LC_ALL is C.UTF-8" test "${LC_ALL:-}" = C.UTF-8 +check "C.UTF-8 locale is usable" bash -c 'locale 2>&1 | grep -vq "Cannot set"' + +## /app layout +check "bootstrap removed itself" test ! -e /app/bin/bootstrap.sh +check "common.sh available to child images" test -r /app/lib/common.sh +check "common.sh MSG prints" bash -c '. /app/lib/common.sh; MSG hello | grep -q hello' +check "common.sh ERR prints to stderr" bash -c '. /app/lib/common.sh; ERR oops 2>&1 >/dev/null | grep -q oops' +check "common.sh CHECK_BIN accepts existing program" bash -c '. /app/lib/common.sh; CHECK_BIN bash' +check "common.sh CHECK_BIN rejects missing program" bash -c '! (. /app/lib/common.sh; CHECK_BIN no-such-program) 2>/dev/null' +check "common.sh CHECK_VAR accepts defined variable" bash -c '. /app/lib/common.sh; FOO=1; CHECK_VAR FOO' +check "common.sh CHECK_VAR rejects undefined variable" bash -c '! (. /app/lib/common.sh; CHECK_VAR NOT_DEFINED) 2>/dev/null' + +## Interactive shell +check "bash.bashrc loads cleanly in an interactive shell" \ + bash -c 'out=$(bash -i -c "echo loaded; declare -p PROMPT_COMMAND" 2>&1