diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f643b86..612806d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -348,8 +348,6 @@ jobs: id: docker_build uses: docker/build-push-action@v6 with: - build-args: | - VERSION=${{ needs.prepare.outputs.source_version }} cache-from: type=local,src=${{ env.BUILDX_CACHE_DIR }} cache-to: type=local,dest=${{ env.BUILDX_CACHE_DIR }} context: . @@ -516,8 +514,6 @@ jobs: id: docker_build uses: docker/build-push-action@v6 with: - build-args: | - VERSION=${{ needs.prepare.outputs.source_version }} cache-from: type=local,src=${{ env.BUILDX_CACHE_DIR }} cache-to: type=local,dest=${{ env.BUILDX_CACHE_DIR }} context: . diff --git a/Dockerfile b/Dockerfile index fc7b5c2..ffcae61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,59 @@ -ARG VERSION=unspecified +# Official Docker images are in the form library/ while non-official +# images are in the form /. +FROM docker.io/library/python:3.13.1-alpine3.20 AS compile-stage -FROM python:3.12.0-alpine +### +# Unprivileged user variables +### +ARG CISA_USER="cisa" +ENV CISA_HOME="/home/${CISA_USER}" +ENV VIRTUAL_ENV="${CISA_HOME}/.venv" -ARG VERSION +# Versions of the Python packages installed directly +ENV PYTHON_PIP_VERSION=24.3.1 +ENV PYTHON_PIPENV_VERSION=2024.4.0 +ENV PYTHON_SETUPTOOLS_VERSION=75.6.0 +ENV PYTHON_WHEEL_VERSION=0.45.1 + +### +# Install the specified versions of pip, setuptools, and wheel into the system +# Python environment; install the specified version of pipenv into the system Python +# environment; set up a Python virtual environment (venv); and install the specified +# versions of pip, setuptools, and wheel into the venv. +# +# Note that we use the --no-cache-dir flag to avoid writing to a local +# cache. This results in a smaller final image, at the cost of +# slightly longer install times. +### +RUN python3 -m pip install --no-cache-dir --upgrade \ + pip==${PYTHON_PIP_VERSION} \ + setuptools==${PYTHON_SETUPTOOLS_VERSION} \ + wheel==${PYTHON_WHEEL_VERSION} \ + && python3 -m pip install --no-cache-dir --upgrade \ + pipenv==${PYTHON_PIPENV_VERSION} \ + # Manually create the virtual environment + && python3 -m venv ${VIRTUAL_ENV} \ + # Ensure the core Python packages are installed in the virtual environment + && ${VIRTUAL_ENV}/bin/python3 -m pip install --no-cache-dir --upgrade \ + pip==${PYTHON_PIP_VERSION} \ + setuptools==${PYTHON_SETUPTOOLS_VERSION} \ + wheel==${PYTHON_WHEEL_VERSION} + +### +# Check the Pipfile configuration and then install the Python dependencies into +# the virtual environment. +# +# Note that pipenv will install into a virtual environment if the VIRTUAL_ENV +# environment variable is set. +### +WORKDIR /tmp +COPY src/Pipfile src/Pipfile.lock ./ +RUN pipenv check --verbose \ + && pipenv install --clear --deploy --extra-pip-args "--no-cache-dir" --verbose + +# Official Docker images are in the form library/ while non-official +# images are in the form /. +FROM docker.io/library/python:3.13.1-alpine3.20 AS build-stage ### # For a list of pre-defined annotation keys and value types see: @@ -27,15 +78,7 @@ ARG CISA_GID=${CISA_UID} ARG CISA_USER="cisa" ENV CISA_GROUP=${CISA_USER} ENV CISA_HOME="/home/${CISA_USER}" - -### -# Upgrade the system -# -# Note that we use apk --no-cache to avoid writing to a local cache. -# This results in a smaller final image, at the cost of slightly -# longer install times. -### -RUN apk --update --no-cache --quiet upgrade +ENV VIRTUAL_ENV="${CISA_HOME}/.venv" ### # Create unprivileged user @@ -44,52 +87,25 @@ RUN addgroup --system --gid ${CISA_GID} ${CISA_GROUP} \ && adduser --system --uid ${CISA_UID} --ingroup ${CISA_GROUP} ${CISA_USER} ### -# Dependencies +# Copy in the Python virtual environment created in compile-stage, symlink the +# Python binary in the venv to the system-wide Python, and add the venv to the PATH. # -# Note that we use apk --no-cache to avoid writing to a local cache. -# This results in a smaller final image, at the cost of slightly -# longer install times. -### -ENV DEPS \ - ca-certificates \ - openssl \ - py-pip -RUN apk --no-cache --quiet add ${DEPS} - -### -# Make sure pip, setuptools, and wheel are the latest versions -# -# Note that we use pip3 --no-cache-dir to avoid writing to a local -# cache. This results in a smaller final image, at the cost of -# slightly longer install times. -### -RUN pip3 install --no-cache-dir --upgrade \ - pip \ - setuptools \ - wheel - -WORKDIR ${CISA_HOME} - -### -# Install Python dependencies -# -# Note that we use pip3 --no-cache-dir to avoid writing to a local -# cache. This results in a smaller final image, at the cost of -# slightly longer install times. -### -RUN wget --output-document sourcecode.tgz \ - https://github.com/cisagov/skeleton-python-library/archive/v${VERSION}.tar.gz \ - && tar --extract --gzip --file sourcecode.tgz --strip-components=1 \ - && pip3 install --no-cache-dir --requirement requirements.txt \ - && ln -snf /run/secrets/quote.txt src/example/data/secret.txt \ - && rm sourcecode.tgz +# Note that we symlink the Python binary in the venv to the system-wide Python so that +# any calls to `python3` will use our virtual environment. We are using short flags +# because the ln binary in Alpine Linux does not support long flags. The -f instructs +# ln to remove the existing file and the -s instructs ln to create a symbolic link. +### +COPY --from=compile-stage --chown=${CISA_USER}:${CISA_GROUP} ${VIRTUAL_ENV} ${VIRTUAL_ENV} +RUN ln -fs "$(command -v python3)" "${VIRTUAL_ENV}"/bin/python3 +ENV PATH="${VIRTUAL_ENV}/bin:$PATH" ### # Prepare to run ### ENV ECHO_MESSAGE="Hello World from Dockerfile" +WORKDIR ${CISA_HOME} USER ${CISA_USER}:${CISA_GROUP} EXPOSE 8080/TCP VOLUME ["/var/log"] ENTRYPOINT ["example"] -CMD ["--log-level", "DEBUG"] +CMD ["--log-level", "DEBUG", "8", "2"] diff --git a/README.md b/README.md index 57f8c30..306552f 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ appropriate for Docker containers and the major languages that we use. To run the `cisagov/example` image via Docker: ```console -docker run cisagov/example:0.0.1 +docker run cisagov/example:0.2.0 ``` ### Running with Docker Compose ### @@ -37,7 +37,7 @@ docker run cisagov/example:0.0.1 services: example: - image: cisagov/example:0.0.1 + image: cisagov/example:0.2.0 volumes: - type: bind source: @@ -82,7 +82,7 @@ environment variables. See the services: example: - image: cisagov/example:0.0.1 + image: cisagov/example:0.2.0 volumes: - type: bind source: @@ -125,23 +125,52 @@ environment variables. See the 1. Pull the new image: ```console - docker pull cisagov/example:0.0.1 + docker pull cisagov/example:0.2.0 ``` 1. Recreate and run the container by following the [previous instructions](#running-with-docker). +## Updating Python dependencies ## + +This image uses [Pipenv] to manage Python dependencies using a [Pipfile](https://github.com/pypa/pipfile). +Both updating dependencies and changing the [Pipenv] configuration in `src/Pipfile` +will result in a modified `src/Pipfile.lock` file that should be committed to the +repository. + +> [!WARNING] +> The `src/Pipfile.lock` as generated will fail `pre-commit` checks due to JSON formatting. + +### Updating dependencies ### + +If you want to update existing dependencies you would run the following command +in the `src/` subdirectory: + +```console +pipenv lock +``` + +### Modifying dependencies ### + +If you want to add or remove dependencies you would update the `src/Pipfile` file +and then update dependencies as you would above. + +> [!NOTE] +> You should only specify packages that are explicitly needed for your Docker +> configuration. Allow [Pipenv] to manage the dependencies of the specified +> packages. + ## Image tags ## The images of this container are tagged with [semantic versions](https://semver.org) of the underlying example project that they containerize. It is recommended that most users use a version tag (e.g. -`:0.0.1`). +`:0.2.0`). | Image:tag | Description | |-----------|-------------| -|`cisagov/example:1.2.3`| An exact release version. | -|`cisagov/example:1.2`| The most recent release matching the major and minor version numbers. | -|`cisagov/example:1`| The most recent release matching the major version number. | +|`cisagov/example:0.2.0`| An exact release version. | +|`cisagov/example:0.2`| The most recent release matching the major and minor version numbers. | +|`cisagov/example:0`| The most recent release matching the major version number. | |`cisagov/example:edge` | The most recent image built from a merge into the `develop` branch of this repository. | |`cisagov/example:nightly` | A nightly build of the `develop` branch of this repository. | |`cisagov/example:latest`| The most recent release image pushed to a container registry. Pulling an image using the `:latest` tag [should be avoided.](https://vsupalov.com/docker-latest-tag/) | @@ -196,8 +225,7 @@ Build the image locally using this git repository as the [build context](https:/ ```console docker build \ - --build-arg VERSION=0.0.1 \ - --tag cisagov/example:0.0.1 \ + --tag cisagov/example:0.2.0 \ https://github.com/cisagov/example.git#develop ``` @@ -227,9 +255,8 @@ Docker: docker buildx build \ --file Dockerfile-x \ --platform linux/amd64 \ - --build-arg VERSION=0.0.1 \ --output type=docker \ - --tag cisagov/example:0.0.1 . + --tag cisagov/example:0.2.0 . ``` ## New repositories from a skeleton ## @@ -256,3 +283,5 @@ dedication](https://creativecommons.org/publicdomain/zero/1.0/). All contributions to this project will be released under the CC0 dedication. By submitting a pull request, you are agreeing to comply with this waiver of copyright interest. + +[Pipenv]: https://pypi.org/project/pipenv/ diff --git a/requirements-dev.txt b/requirements-dev.txt index de5eb3b..d7a04ed 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ --requirement requirements-test.txt ipython +pipenv semver>=3 diff --git a/src/Pipfile b/src/Pipfile new file mode 100644 index 0000000..fdd19e6 --- /dev/null +++ b/src/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +# List any Python dependencies for the image here +[packages] +# This should match the version of the image +example = {file = "https://github.com/cisagov/skeleton-python-library/archive/v0.2.0.tar.gz"} + +# This version should match the version of Python in the image +[requires] +python_full_version = "3.13.1" diff --git a/src/Pipfile.lock b/src/Pipfile.lock new file mode 100644 index 0000000..408d508 --- /dev/null +++ b/src/Pipfile.lock @@ -0,0 +1,45 @@ +{ + "_meta": { + "hash": { + "sha256": "8a376df6f25cf8583d5da89da420c5e51660f33a081c1f85236643ef31601833" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.13.1" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "example": { + "file": "https://github.com/cisagov/skeleton-python-library/archive/v0.2.0.tar.gz" + }, + "schema": { + "hashes": [ + "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde", + "sha256:7da553abd2958a19dc2547c388cde53398b39196175a9be59ea1caf5ab0a1807" + ], + "version": "==0.7.7" + }, + "setuptools": { + "hashes": [ + "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", + "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d" + ], + "markers": "python_version >= '3.9'", + "version": "==75.6.0" + } + }, + "develop": {} +} diff --git a/src/version.txt b/src/version.txt index 8acdd82..0ea3a94 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -0.0.1 +0.2.0 diff --git a/tests/container_test.py b/tests/container_test.py index cf18333..bddee4b 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -10,9 +10,8 @@ ENV_VAR = "ECHO_MESSAGE" ENV_VAR_VAL = "Hello World from docker compose!" READY_MESSAGE = "This is a debug message" -SECRET_QUOTE = ( - "There are no secrets better kept than the secrets everybody guesses." # nosec -) +DIVISION_MESSAGE = "8 / 2 == 4.000000" +SECRET_QUOTE = "Three may keep a secret, if two of them are dead." # nosec RELEASE_TAG = os.getenv("RELEASE_TAG") VERSION_FILE = "src/version.txt" @@ -54,6 +53,7 @@ def test_output(dockerc, main_container): # make sure container exited if running test isolated dockerc.wait(main_container.id) log_output = main_container.logs() + assert DIVISION_MESSAGE in log_output, "Division message not found in log output." assert SECRET_QUOTE in log_output, "Secret not found in log output."