├── entrypoint.sh ├── ci ├── docker │ ├── static-container-registry │ │ └── docker-test-entrypoint.sh │ ├── image-provisioner │ │ ├── Dockerfile │ │ └── provision-images.sh │ └── sut │ │ └── Dockerfile └── before-install.sh ├── Dockerfile ├── .travis.yml ├── docker-compose.test.yml ├── .gitignore ├── test.sh ├── README.md ├── static-container-registry.py └── LICENSE /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ue 4 | 5 | python3 /static-container-registry.py /var/lib/images > /var/run/static-container-registry.conf 6 | 7 | exec "$@" 8 | -------------------------------------------------------------------------------- /ci/docker/static-container-registry/docker-test-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ue 4 | 5 | printf "Waiting for images to be pulled..." 1>&2 6 | while [ ! -f /var/lib/images/.pulled ]; do 7 | sleep 0.5 8 | done 9 | echo " done" 1>&2 10 | 11 | exec /entrypoint.sh nginx -g 'daemon off;' 12 | -------------------------------------------------------------------------------- /ci/docker/image-provisioner/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/ubuntu:xenial 2 | 3 | RUN apt-get update \ 4 | && \ 5 | apt-get install -y software-properties-common\ 6 | && \ 7 | add-apt-repository ppa:projectatomic/ppa \ 8 | && \ 9 | apt-get update \ 10 | && \ 11 | apt-get install -y skopeo 12 | 13 | RUN apt-get install \ 14 | hardlink 15 | 16 | COPY provision-images.sh /provision-images.sh 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/nginx:1.15.12-alpine 2 | 3 | RUN apk add --no-cache \ 4 | python3 \ 5 | && \ 6 | sed -i \ 7 | '/location \/ {/i include\ \/var\/run\/static-container-registry.conf;' \ 8 | /etc/nginx/conf.d/default.conf 9 | 10 | VOLUME /var/lib/images 11 | COPY entrypoint.sh /entrypoint.sh 12 | COPY static-container-registry.py /static-container-registry.py 13 | 14 | ENTRYPOINT ["/entrypoint.sh"] 15 | CMD ["nginx", "-g", "daemon off;"] 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | dist: xenial 3 | 4 | services: 5 | - docker 6 | 7 | addons: 8 | apt: 9 | sources: 10 | - sourceline: 'ppa:projectatomic/ppa' 11 | - sourceline: 'deb https://pgrange.github.io/bash-unit_deb/debian/ unstable/' 12 | key_url: 'https://pgrange.github.io/bash-unit_deb/keys.asc' 13 | packages: 14 | - bash-unit 15 | - cri-o-1.12 16 | - hardlink 17 | 18 | before_install: 19 | - ./ci/before-install.sh 20 | 21 | script: 22 | - bash_unit ./test.sh 23 | -------------------------------------------------------------------------------- /ci/docker/sut/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/ubuntu:xenial 2 | 3 | RUN apt-get update \ 4 | && \ 5 | apt-get install -y software-properties-common\ 6 | && \ 7 | add-apt-repository ppa:projectatomic/ppa \ 8 | && \ 9 | apt-get update \ 10 | && \ 11 | apt-get install -y skopeo 12 | 13 | RUN apt-get update \ 14 | && \ 15 | apt-get install -y \ 16 | apt-transport-https \ 17 | curl \ 18 | && \ 19 | curl https://pgrange.github.io/bash-unit_deb/keys.asc | apt-key add - \ 20 | && \ 21 | echo deb https://pgrange.github.io/bash-unit_deb/debian/ unstable/ | \ 22 | tee -a /etc/apt/sources.list.d/bash-unit.list \ 23 | && \ 24 | apt-get update \ 25 | && \ 26 | apt-get install bash-unit 27 | 28 | COPY test.sh / 29 | -------------------------------------------------------------------------------- /ci/docker/image-provisioner/provision-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ue -o pipefail 4 | 5 | rm -f /var/lib/images/.pulled 6 | 7 | HARDLINK=${HARDLINK:-$(command -v hardlink || echo false)} 8 | SKOPEO=${SKOPEO:-$(command -v skopeo || echo false)} 9 | IMAGES=/var/lib/images 10 | 11 | test -d $IMAGES 12 | 13 | mkdir -p $IMAGES/alpine $IMAGES/metalk8s-keepalived 14 | 15 | $SKOPEO copy --format v2s2 --dest-compress docker://docker.io/alpine:3.9.3 dir:$IMAGES/alpine/3.9.3 16 | $SKOPEO copy --format v2s2 --dest-compress docker://docker.io/alpine:3.9 dir:$IMAGES/alpine/3.9 17 | $SKOPEO copy --format v2s2 --dest-compress docker://docker.io/alpine:3.8.4 dir:$IMAGES/alpine/3.8.4 18 | $SKOPEO copy --format v2s2 --dest-compress docker://docker.io/nicolast/metalk8s-keepalived:latest dir:$IMAGES/metalk8s-keepalived/latest 19 | 20 | $HARDLINK -c -vv /var/lib/images 21 | 22 | touch /var/lib/images/.pulled 23 | -------------------------------------------------------------------------------- /docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | static-container-registry: 5 | build: . 6 | entrypoint: /docker-test-entrypoint.sh 7 | volumes: 8 | - type: volume 9 | source: images 10 | target: /var/lib/images 11 | - type: bind 12 | source: ./ci/docker/static-container-registry/docker-test-entrypoint.sh 13 | target: /docker-test-entrypoint.sh 14 | depends_on: 15 | - image-provisioner 16 | 17 | image-provisioner: 18 | build: ci/docker/image-provisioner 19 | command: /provision-images.sh 20 | volumes: 21 | - type: volume 22 | source: images 23 | target: /var/lib/images 24 | 25 | sut: 26 | build: 27 | context: . 28 | dockerfile: ci/docker/sut/Dockerfile 29 | command: bash_unit /test.sh 30 | depends_on: 31 | - static-container-registry 32 | environment: 33 | IMAGE: '' 34 | IMAGES: '' 35 | REGISTRY_HOST: 'static-container-registry' 36 | REGISTRY_PORT: '80' 37 | TEST_DOCKER: '0' 38 | TEST_CRIO: '0' 39 | TEST_CONTAINERD: '0' 40 | TEST_SKOPEO: '1' 41 | 42 | volumes: 43 | images: 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /ci/before-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SKOPEO_VERSION=0.1.38-1~ubuntu16.04~ppa1 4 | CONTAINERD_VERSION=1.2.6 5 | CRICTL_VERSION=1.14.0 6 | 7 | set -xue -o pipefail 8 | 9 | SKOPEO_DEB=skopeo_${SKOPEO_VERSION}_amd64.deb 10 | CONTAINERD_ARCHIVE=containerd-${CONTAINERD_VERSION}.linux-amd64.tar.gz 11 | CRICTL_ARCHIVE=crictl-v${CRICTL_VERSION}-linux-amd64.tar.gz 12 | 13 | curl -LO https://launchpad.net/~projectatomic/+archive/ubuntu/ppa/+files/${SKOPEO_DEB} 14 | dpkg -x ${SKOPEO_DEB} skopeo/ 15 | sudo cp skopeo/usr/bin/skopeo /usr/local/bin/skopeo 16 | rm -r skopeo ${SKOPEO_DEB} 17 | 18 | curl -LO https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/${CONTAINERD_ARCHIVE} 19 | tar xvf ${CONTAINERD_ARCHIVE} 20 | sudo mv bin/* /usr/bin/ 21 | rm ${CONTAINERD_ARCHIVE} 22 | 23 | cat << EOF | sudo tee /etc/systemd/system/containerd.service 24 | [Unit] 25 | Description=containerd container runtime 26 | Documentation=https://containerd.io 27 | After=network.target 28 | 29 | [Service] 30 | ExecStartPre=-/sbin/modprobe overlay 31 | ExecStart=/usr/bin/containerd 32 | 33 | Delegate=yes 34 | KillMode=process 35 | # Having non-zero Limit*s causes performance problems due to accounting overhead 36 | # in the kernel. We recommend using cgroups to do container-local accounting. 37 | LimitNPROC=infinity 38 | LimitCORE=infinity 39 | LimitNOFILE=1048576 40 | # Comment TasksMax if your systemd version does not supports it. 41 | # Only systemd 226 and above support this version. 42 | TasksMax=infinity 43 | 44 | [Install] 45 | WantedBy=multi-user.target 46 | EOF 47 | 48 | sudo systemctl daemon-reload 49 | 50 | sudo mkdir -p /etc/containerd 51 | cat << EOF | sudo tee /etc/containerd/config.toml 52 | [plugins] 53 | [plugins.cri] 54 | [plugins.cri.registry] 55 | [plugins.cri.registry.mirrors] 56 | [plugins.cri.registry.mirrors."127.0.0.1:5000"] 57 | endpoint = ["http://127.0.0.1:5000"] 58 | EOF 59 | 60 | sudo systemctl start containerd 61 | 62 | sudo systemctl start crio 63 | 64 | curl -LO https://github.com/kubernetes-sigs/cri-tools/releases/download/v${CRICTL_VERSION}/${CRICTL_ARCHIVE} 65 | tar xvf ${CRICTL_ARCHIVE} crictl 66 | sudo mv crictl /usr/local/bin/crictl 67 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | # This file is to be executed using `bash_unit` 2 | 3 | DOCKER=${DOCKER:-$(command -v docker || echo false)} 4 | SKOPEO=${SKOPEO:-$(command -v skopeo || echo false)} 5 | CRICTL=${CRICTL:-$(command -v crictl || echo false)} 6 | CURL=${CURL:-$(command -v curl || echo false)} 7 | HARDLINK=${HARDLINK:-$(command -v hardlink || echo false)} 8 | 9 | IMAGE=${IMAGE-nicolast/static-container-registry:test} 10 | IMAGES=${IMAGES-/tmp/images} 11 | CONTAINER_NAME=${CONTAINER_NAME-static-container-registry-test} 12 | REGISTRY_HOST=${REGISTRY_HOST-127.0.0.1} 13 | REGISTRY_PORT=${REGISTRY_PORT-5000} 14 | REGISTRY="$REGISTRY_HOST:$REGISTRY_PORT" 15 | 16 | TEST_DOCKER=${TEST_DOCKER:-1} 17 | TEST_CONTAINERD=${TEST_CONTAINERD:-1} 18 | TEST_CRIO=${TEST_CRIO:-1} 19 | TEST_SKOPEO=${TEST_SKOPEO:-1} 20 | 21 | if [ "$TEST_DOCKER" -eq 1 ]; then 22 | test_docker() { 23 | for image in ${AVAILABLE_IMAGES[*]}; do 24 | assert "$DOCKER pull '$REGISTRY/$image'" 25 | done 26 | } 27 | fi 28 | 29 | if [ "$TEST_CONTAINERD" -eq 1 ]; then 30 | test_containerd() { 31 | for image in ${AVAILABLE_IMAGES[*]}; do 32 | assert "sudo ${CRICTL} --image-endpoint unix:///run/containerd/containerd.sock pull '$REGISTRY/$image'" 33 | done 34 | } 35 | fi 36 | 37 | if [ "$TEST_CRIO" -eq 1 ]; then 38 | test_crio() { 39 | for image in ${AVAILABLE_IMAGES[*]}; do 40 | assert "sudo ${CRICTL} --image-endpoint unix:///run/crio/crio.sock pull '$REGISTRY/$image'" 41 | done 42 | } 43 | fi 44 | 45 | if [ "$TEST_SKOPEO" -eq 1 ]; then 46 | test_skopeo() { 47 | for image in ${AVAILABLE_IMAGES[*]}; do 48 | assert "$SKOPEO --debug inspect --tls-verify=false 'docker://$REGISTRY/$image'" 49 | done 50 | } 51 | fi 52 | 53 | setup_suite() { 54 | if [ "x$IMAGE" != "x" ]; then 55 | assert build_project_image 56 | fi 57 | if [ "x$IMAGES" != "x" ]; then 58 | assert create_images_directory 59 | fi 60 | } 61 | 62 | teardown_suite() { 63 | if [ "x$IMAGES" != "x" ]; then 64 | remove_images_directory 65 | fi 66 | if [ "x$IMAGE" != "x" ]; then 67 | delete_project_image 68 | fi 69 | } 70 | 71 | setup() { 72 | if [ "x$IMAGE" != "x" ]; then 73 | $DOCKER run \ 74 | -d \ 75 | -p "$REGISTRY:80" \ 76 | -v "$IMAGES:/var/lib/images:ro" \ 77 | --name "$CONTAINER_NAME" \ 78 | "$IMAGE" > /dev/null 79 | fi 80 | 81 | local i=600 82 | while [ $i -gt 0 ]; do 83 | local ok 84 | ok=$($CURL --silent "http://$REGISTRY/v2/" 2>/dev/null) 85 | if [ "x$ok" = 'xok' ]; then 86 | i=0 87 | else 88 | sleep 0.1 89 | i=$((i - 1)) 90 | fi 91 | done 92 | } 93 | 94 | teardown() { 95 | if [ "x$IMAGE" != "x" ]; then 96 | $DOCKER stop "$CONTAINER_NAME" > /dev/null 97 | $DOCKER rm "$CONTAINER_NAME" > /dev/null 98 | fi 99 | } 100 | 101 | build_project_image() { 102 | $DOCKER build -t "$IMAGE" . 103 | } 104 | 105 | delete_project_image() { 106 | $DOCKER rmi "$IMAGE" > /dev/null 107 | } 108 | 109 | AVAILABLE_IMAGES=( 110 | 'alpine:3.9.3' 111 | 'alpine:3.9' 112 | 'alpine:3.8.4' 113 | 'metalk8s-keepalived:latest' 114 | ) 115 | create_images_directory() { 116 | mkdir "$IMAGES" "$IMAGES/alpine" "$IMAGES/metalk8s-keepalived" 117 | $SKOPEO copy --format v2s2 --dest-compress \ 118 | docker://docker.io/alpine:3.9.3 \ 119 | "dir:$IMAGES/alpine/3.9.3" 120 | $SKOPEO copy --format v2s2 --dest-compress \ 121 | docker://docker.io/alpine:3.9 \ 122 | "dir:$IMAGES/alpine/3.9" 123 | $DOCKER pull docker.io/alpine:3.8.4 124 | $SKOPEO copy --format v2s2 --dest-compress \ 125 | docker-daemon:alpine:3.8.4 \ 126 | "dir:$IMAGES/alpine/3.8.4" 127 | $DOCKER rmi docker.io/alpine:3.8.4 128 | $SKOPEO copy --format v2s2 --dest-compress \ 129 | docker://docker.io/nicolast/metalk8s-keepalived:latest \ 130 | "dir:$IMAGES/metalk8s-keepalived/latest" 131 | 132 | $HARDLINK -c -vv "${IMAGES}" 133 | } 134 | 135 | remove_images_directory() { 136 | rm -rf "$IMAGES" 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | static-container-registry 2 | ========================= 3 | This is a set of scripts to create a Docker-compatible read-only 'registry' that 4 | can be served by a static Nginx HTTP server, without the need to run a 5 | full-fledged registry solution. 6 | 7 | Getting Started 8 | --------------- 9 | First, create a directory that'll contain all images that should be part of the 10 | registry, e.g. 11 | 12 | ``` 13 | $ mkdir images/ 14 | ``` 15 | 16 | Then, for every image you want to serve, fetch the image using `skopeo` into a 17 | `dir` target. For every image `name:tag`, create a directory `name` in the root 18 | directory, and let `skopeo` copy the image into `name/tag`: 19 | 20 | ``` 21 | $ mkdir images/alpine images/metalk8s-keepalived 22 | $ skopeo copy --format v2s2 --dest-compress docker://docker.io/alpine:3.9.3 dir:images/alpine/3.9.3 23 | $ skopeo copy --format v2s2 --dest-compress docker://docker.io/alpine:3.9 dir:images/alpine/3.9 24 | $ skopeo copy --format v2s2 --dest-compress docker-daemon:alpine:3.8.4 dir:images/alpine/3.8.4 25 | $ skopeo copy --format v2s2 --dest-compress docker://docker.io/nicolast/metalk8s-keepalived:latest dir:images/metalk8s-keepalived/latest 26 | ``` 27 | 28 | For extra credits, we tell `skopeo` to compress all layers. 29 | 30 | In the example above, we pulled Alpine 3.9(.3) twice. As a result, the same 31 | files are now stored multiple files on the system. If many of your images use 32 | the same base image(s), this can quickly add up. Luckily, there's an easy way to 33 | reduce this overhead since these files are always immutable: use hardlinks! 34 | There's a tool which does exactly this, aptly called `hardlink`: 35 | 36 | ``` 37 | $ hardlink -c -vv images 38 | Linked images/metalk8s-keepalived/latest/version to images/alpine/3.9.3/version, saved 33 39 | Linked images/metalk8s-keepalived/latest/version to images/alpine/3.8.4/version, saved 33 40 | Linked images/alpine/3.9.3/cdf98d1859c1beb33ec70507249d34bacf888d59c24df3204057f9a6c758dddb to images/alpine/3.9/cdf98d1859c1beb33ec70507249d34bacf888d59c24df3204057f9a6c758dddb, saved 1512 41 | Linked images/metalk8s-keepalived/latest/version to images/alpine/3.9/version, saved 33 42 | Linked images/alpine/3.9.3/bdf0201b3a056acc4d6062cc88cd8a4ad5979983bfb640f15a145e09ed985f92 to images/alpine/3.9/bdf0201b3a056acc4d6062cc88cd8a4ad5979983bfb640f15a145e09ed985f92, saved 2757009 43 | Linked images/alpine/3.9.3/manifest.json to images/alpine/3.9/manifest.json, saved 528 44 | 45 | 46 | Directories 7 47 | Objects 24 48 | IFREG 17 49 | Comparisons 7 50 | Linked 6 51 | saved 2781184 52 | ``` 53 | 54 | Now we're ready to create an Nginx configuration file that can be `include`d in 55 | a larger configuration: 56 | 57 | ``` 58 | $ ./static-container-registry.py ./images > registry.conf 59 | ``` 60 | 61 | The following options are available: 62 | 63 | - `--name-prefix PREFIX` will prefix `PREFIX` to every container name. As an 64 | example, given the layout above, setting this to `myproject` would make 65 | `docker pull registry.domain.tld/myproject/alpine:3.9` work, instead of `docker pull 66 | registry.domain.tld/alpine:3.9`. 67 | - `--server-root PATH` tells the script where the image files will be stored on 68 | the web-server. This defaults to the provided image path when the script is 69 | executed. Hint: this can be any string, including a variable name (e.g. 70 | `$registry_root`, though remember to take care of shell quoting!), which can 71 | then be defined (`set $registry_root /path/to/images`) in another Nginx 72 | configuration file). 73 | - Finally, the positional argument must be the path to the image files. This can 74 | be unspecified, which will then default to the current working directory. 75 | 76 | All that's left to be done is firing up `nginx` with the configuration 77 | `include`d. 78 | 79 | Using Docker 80 | ------------ 81 | A Docker container image for this project is automatically built 82 | [on DockerHub](https://hub.docker.com/r/nicolast/static-container-registry). 83 | To use this image, first create a directory containing all required image blobs 84 | (see above), then run 85 | 86 | ``` 87 | $ docker run \ 88 | --name static-oci-registry \ 89 | -p 127.0.0.1:80:80 \ 90 | --mount type=bind,source=/absolute/path/to/images,destination=/var/lib/images,ro \ 91 | --rm \ 92 | --read-only \ 93 | --mount type=tmpfs,destination=/run \ 94 | --mount type=tmpfs,destination=/var/cache/nginx \ 95 | docker.io/nicolast/static-container-registry:latest 96 | ``` 97 | 98 | Make sure to replace the path to the `images`, which should be exposed at 99 | `/var/lib/images` to the container. 100 | 101 | Goals and non-goals 102 | ------------------- 103 | This tool is supposed to 'implement' the Docker distribution APIs to the extent 104 | required for `docker pull` (and other container runtimes and tools) to work. 105 | This does not necessarily imply all subtle details of the distribution API, 106 | including error reporting, are fully implemented. 107 | 108 | This tool does not, and will never, support uploads (`push`) of new images. 109 | 110 | Thanks 111 | ------ 112 | - [@mtrmac](https://github.com/mtrmac) for hinting at using the `dir` target of 113 | `skopeo` in 114 | [#469](https://github.com/containers/skopeo/issues/469#issuecomment-465353019) 115 | - [@rhatdan](https://github.com/rhatdan) and the `skopeo` team for `skopeo` 116 | -------------------------------------------------------------------------------- /static-container-registry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import os.path 5 | import json 6 | import hashlib 7 | import logging 8 | import argparse 9 | 10 | 11 | LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | CONSTANTS = ''' 15 | location = /v2 {{ 16 | return 301 /v2/; 17 | }} 18 | 19 | location = /v2/ {{ 20 | return 200 'ok'; 21 | }} 22 | 23 | location @404_tag {{ 24 | internal; 25 | types {{ }} default_type "application/json"; 26 | return 404 '{tag_invalid:s}'; 27 | }} 28 | '''.format( 29 | tag_invalid=json.dumps({ 30 | 'errors': [{ 31 | 'code': 'TAG_INVALID', 32 | 'message': 'manifest tag did not match URI', 33 | 'detail': '', 34 | }] 35 | }), 36 | ) 37 | 38 | 39 | MANIFEST_JSON = 'manifest.json' 40 | 41 | 42 | def find_images(root): 43 | LOGGER.info('Finding images in %s', root) 44 | 45 | for name in os.listdir(root): 46 | curr = os.path.join(root, name) 47 | LOGGER.info('Looking into %s for tags of %s', curr, name) 48 | 49 | if not os.path.isdir(curr): 50 | continue 51 | 52 | for tag in os.listdir(curr): 53 | curr = os.path.join(root, name, tag) 54 | 55 | if not os.path.isdir(curr): 56 | LOGGER.info('Not a directory: %s', curr) 57 | continue 58 | 59 | LOGGER.info('Looking into %s for a valid image', curr) 60 | 61 | manifest = os.path.join(curr, MANIFEST_JSON) 62 | 63 | if not os.path.isfile(manifest): 64 | LOGGER.info('No manifest file at %s', manifest) 65 | continue 66 | 67 | with open(manifest, 'r') as fd: 68 | LOGGER.info('Attempting to load JSON data from %s', manifest) 69 | try: 70 | data = json.load(fd) 71 | except json.JSONDecodeError: 72 | LOGGER.info('Failed to decode JSON from %s', manifest) 73 | data = None 74 | 75 | if not data: 76 | continue 77 | 78 | if data.get('schemaVersion') != 2: 79 | LOGGER.info('Invalid schemaVersion in %s', manifest) 80 | continue 81 | 82 | if data.get('mediaType') != \ 83 | 'application/vnd.docker.distribution.manifest.v2+json': 84 | LOGGER.info('Invalid mediaType in %s', manifest) 85 | continue 86 | 87 | LOGGER.info('Found image %s:%s in %s', name, tag, curr) 88 | yield (name, tag) 89 | 90 | 91 | def create_config(root, server_root, name_prefix, with_constants=True, 92 | only_constants=False): 93 | if with_constants: 94 | yield CONSTANTS 95 | 96 | if only_constants: 97 | return 98 | 99 | images = {} 100 | for (name, tag) in find_images(root): 101 | images.setdefault(name, set()).add(tag) 102 | 103 | for (name, tags) in sorted(images.items()): 104 | tag_list = { 105 | 'name': name, 106 | 'tags': sorted(tags), 107 | } 108 | 109 | yield ''' 110 | location = /v2/{name_prefix:s}{name:s}/tags/list {{ 111 | types {{ }} default_type "application/json"; 112 | return 200 '{payload:s}'; 113 | }} 114 | '''.format( 115 | name=name, 116 | name_prefix=name_prefix.lstrip('/'), 117 | payload=json.dumps(tag_list), 118 | ) 119 | 120 | seen_digests = set() 121 | 122 | for tag in sorted(tags): 123 | manifest_file = os.path.join(root, name, tag, MANIFEST_JSON) 124 | 125 | digest = hashlib.sha256() 126 | 127 | with open(manifest_file, 'rb') as fd: 128 | for chunk in iter(lambda: fd.read(4096), b''): 129 | digest.update(chunk) 130 | 131 | hexdigest = digest.hexdigest() 132 | 133 | yield ''' 134 | location = "/v2/{name_prefix:s}{name:s}/manifests/{tag:s}" {{ 135 | alias {server_root:s}/{name:s}/{tag:s}/; 136 | types {{ }} default_type "application/vnd.docker.distribution.manifest.v2+json"; 137 | add_header 'Docker-Content-Digest' 'sha256:{digest:s}'; 138 | try_files manifest.json =404; 139 | error_page 404 @404_tag; 140 | }} 141 | '''.format( 142 | name=name, 143 | tag=tag, 144 | name_prefix=name_prefix.lstrip('/'), 145 | digest=hexdigest, 146 | server_root=server_root, 147 | ) 148 | 149 | if hexdigest not in seen_digests: 150 | yield ''' 151 | location = "/v2/{name_prefix:s}{name:s}/manifests/sha256:{digest:s}" {{ 152 | alias {server_root:s}/{name:s}/{tag:s}/; 153 | types {{ }} default_type "application/vnd.docker.distribution.manifest.v2+json"; 154 | add_header 'Docker-Content-Digest' 'sha256:{digest:s}'; 155 | try_files manifest.json =404; 156 | error_page 404 @404_tag; 157 | }} 158 | '''.format( 159 | name=name, 160 | tag=tag, 161 | name_prefix=name_prefix.lstrip('/'), 162 | digest=hexdigest, 163 | server_root=server_root, 164 | ) 165 | else: 166 | yield ''' 167 | # Digest for "{name:s}:{tag:s}" already served 168 | '''.format( 169 | name=name, 170 | tag=tag, 171 | ) 172 | 173 | seen_digests.add(hexdigest) 174 | 175 | yield ''' 176 | location ~ "/v2/{name_prefix:s}{name:s}/blobs/sha256:([a-f0-9]{{64}})" {{ 177 | alias {server_root:s}/{name:s}/; 178 | try_files {paths:s} =404; 179 | }} 180 | '''.format( 181 | name_prefix=name_prefix.lstrip('/'), 182 | server_root=server_root, 183 | name=name, 184 | paths=' '.join('{tag:s}/$1'.format(tag=tag) for tag in sorted(tags)), 185 | ) 186 | 187 | 188 | def main(): 189 | logging.basicConfig( 190 | level=logging.INFO, 191 | ) 192 | 193 | parser = argparse.ArgumentParser() 194 | parser.add_argument( 195 | '--name-prefix', 196 | metavar='PREFIX', 197 | help='optional prefix added to every image name', 198 | ) 199 | 200 | constants_group = parser.add_mutually_exclusive_group() 201 | constants_group.add_argument( 202 | '--omit-constants', 203 | action='store_true', 204 | help='do not write rules for constant locations (e.g. /v2/),' 205 | ' necessary to include the configuration with others.', 206 | ) 207 | constants_group.add_argument( 208 | '--only-constants', 209 | action='store_true', 210 | help='only write rules for constant locations (e.g. /v2/),' 211 | ' to include only once.', 212 | ) 213 | 214 | root = os.getcwd() 215 | parser.add_argument( 216 | '--server-root', 217 | metavar='PATH', 218 | help='root directory from where exported image files are served' \ 219 | ' (default: ROOT)' 220 | ) 221 | parser.add_argument( 222 | 'root', 223 | metavar='ROOT', 224 | nargs='?', 225 | default=root, 226 | help='root directory containing exported images (default: {})'.format( 227 | root), 228 | ) 229 | 230 | args = parser.parse_args() 231 | 232 | name_prefix = '{}/'.format((args.name_prefix or '').strip('/')) 233 | with_constants = not args.omit_constants 234 | only_constants = args.only_constants 235 | root = os.path.abspath(args.root) 236 | server_root = args.server_root or root 237 | 238 | logging.debug('Name prefix: %s', name_prefix) 239 | logging.debug('Server root: %s', server_root) 240 | logging.debug('Root: %s', root) 241 | 242 | config_gen = create_config( 243 | root, server_root, name_prefix, with_constants, only_constants 244 | ) 245 | for part in config_gen: 246 | sys.stdout.write(part) 247 | 248 | 249 | if __name__ == '__main__': 250 | main() 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------