######
Prefab
######

**Build container images faster** ⚡️

.. image:: https://imgs.xkcd.com/comics/compiling.png
    :target: https://xkcd.com/license.html
    :alt: https://xkcd.com/303/

|

.. image:: https://github.com/lexsca/prefab/actions/workflows/checks.yml/badge.svg
    :target: https://github.com/lexsca/prefab/actions/workflows/checks.yml

.. image:: https://img.shields.io/pypi/v/container-prefab.svg
    :target: https://pypi.org/project/container-prefab/

.. image:: https://img.shields.io/pypi/pyversions/container-prefab.svg
    :target: https://pypi.org/project/container-prefab/

.. image:: https://img.shields.io/github/license/lexsca/prefab.svg
    :target: https://github.com/lexsca/prefab/blob/master/LICENSE

.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
    :target: https://github.com/psf/black

|

*Prefab* is a Python-based container image build tool that uses deterministic remote caching to reduce build times.  Unlike `BuildKit <https://github.com/moby/buildkit#cache>`_ and the `Docker CLI <https://docs.docker.com/engine/reference/commandline/build/#specifying-external-cache-sources>`_, which use build layer caching, *Prefab* uses whole image caching based on a digest of the Dockerfile in combination with digests of specified files and directory trees.  This allows *Prefab* to check for and pull cached images before resorting to building a new image.


Quickstart
==========

Look at the `example directory <https://github.com/lexsca/prefab/tree/main/example>`_ to see how to build an example app with *Prefab*.


Installation and usage
======================

*Prefab* can be installed and run in three different ways:

#. Local Python package
#. Docker outside of Docker (DooD) container
#. Docker in Docker (DinD) container

Use whichever mode works best for the use-case(s) at hand.  Each supports the same CLI arguments:

CLI arguments
-------------

::

    usage: prefab [-h] [--config PATH] [--dry-run] [--force] [--monochrome]
                  [--push TARGET_NAME [TARGET_NAME ...]] [--push-all] --repo URI
                  --target TARGET_NAME[:TAG] [TARGET_NAME[:TAG] ...]

    Build container images faster ⚡️

    optional arguments:
      -h, --help            show this help message and exit
      --config PATH, -c PATH
                            Target build config file to use (default: prefab.yml)
      --dry-run             Show how targets would be built (implies --force)
      --force               Force target(s) to be rebuilt
      --monochrome, -m      Don't colorize log messages
      --push TARGET_NAME [TARGET_NAME ...], -p TARGET_NAME [TARGET_NAME ...]
                            Image target(s) to push to repo after building
      --push-all            Push all image targets to repo after building
      --repo URI, -r URI    Image repo to use (e.g. lexsca/prefab)
      --target TARGET_NAME[:TAG] [TARGET_NAME[:TAG] ...], -t TARGET_NAME[:TAG] [TARGET_NAME[:TAG] ...]
                            Image target(s) to build with optional custom image tag


Local Python package
--------------------

To install *Prefab* as a local Python package::

    pip install container-prefab

To run *Prefab* as a local Python package to build an push a build target::

    prefab --repo repo.tld/org/project --push --target name

NOTE: Container images now hosted on Docker Hub
-----------------------------------------------

The container images below used to be hosted by Quay and are now
hosted by `Docker Hub <https://hub.docker.com/r/lexsca/prefab>`_
This decision was not taken lightly. Sadly, Quay has not proven to
be a reliable service. The final straw was when RedHat acquired
them and broke authentication.

Docker outside of Docker (DooD)
-------------------------------

To get the *Prefab* Docker outside of Docker (DooD) image::

    docker pull lexsca/prefab:dood

To run the *Prefab* Docker outside of Docker image to build an push a build target::

    docker run --rm -it -v $(/bin/pwd):/build -w /build \
        -e REGISTRY_AUTH=$(jq -c . ~/.docker/config.json | base64) \
        -v /var/run/docker.sock:/docker.sock \
        lexsca/prefab:dood --repo repo.tld/org/project \
        --push --target name

Docker in Docker (DinD)
-----------------------

To get the *Prefab* Docker in Docker (DinD) image::

    docker pull lexsca/prefab:dind

To run the *Prefab* Docker in Docker image to build an push a build target::

    docker run --rm -it -v $(/bin/pwd):/build -w /build --privileged \
        -e REGISTRY_AUTH=$(jq -c . ~/.docker/config.json | base64) \
        lexsca/prefab:dind --repo repo.tld/org/project \
        --push --target name

Configuration
=============

*Prefab* uses a `YAML <https://en.wikipedia.org/wiki/YAML>`_ configuration file to determine which container images to build for a given target and in which order.  This configuration below is taken from the `example directory <https://github.com/lexsca/prefab/tree/main/example>`_ in this repo.

The ``prefab.yml`` file has two build targets, each with a Dockerfile. The ``app`` target has a dependency on the ``packages`` target, so it's built or pulled first, before building the ``app`` target.  This is a simple example, but the dependency graph can be arbitrarily deep or wide for complex build targets.

``prefab.yml``
--------------

::

    targets:

      app:
        dockerfile: Dockerfile.app
        depends_on:
          - packages
        watch_files:
          - app.py

      packages:
        dockerfile: Dockerfile.packages

When building a container image, *Prefab* populates `build arguments <https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg>`_ for each build target depndency, uppercased by convention, and prefixed with ``PREFAB_`` to avoid conflicts with other build arguments.


``Dockerfile.app``
------------------

::

    ARG PREFAB_PACKAGES

    FROM $PREFAB_PACKAGES as packages

Contributing
============

Bug reports are welcome.  Pull requests even more so.

Before making any changes, first ensure the development environment is functional and the extant linting and tests are passing.  To start a development environment, clone or fork this source repo and follow the instructions below.

Alternatively, it's fine to create a virtual environment an install packages from ``requirements.txt`` and ``requirements-dev.txt`` files. The Python version should be 3.7 or later.

Prerequisites
-------------

#. POSIX Shell (e.g. bash)
#. Docker
#. GNU Make

Create environment
------------------

To create a development runtime environment::

    $ make bootstrap

The above will create a minimal environment that will allow *Prefab* to build its development environment image.  This image can be used to run linting and tests::

    $ docker images lexsca/prefab:dev
    REPOSITORY      TAG                 IMAGE ID            CREATED              SIZE
    lexsca/prefab   dev                 ddee1cafb775        About a minute ago   429MB

Use environment
---------------

Once created, the development image can used via::

    $ make shell
    docker run --rm -it -v /Users/lexsca/git/prefab:/prefab -w /prefab \
            -v /var/run/docker.sock:/docker.sock -e PYTHONPATH=/prefab/lib \
            --entrypoint /bin/bash lexsca/prefab:dev --login -o vi
    3053ae861610:/prefab# make test

This will mount the docker socket and current working directory in an environment where tests can be run, dependencies built, or a debugger invoked to aid in iterating.

The ``make test`` command should pass before attempting to submit any code changes.
