feature/11-update_master_to_beta_release_commits #12

Merged
alexvanin merged 185 commits from r.loginov/distribution:feature/11-update_master_to_beta_release_commits into tcl/master 2024-08-19 12:13:20 +00:00
119 changed files with 4211 additions and 1321 deletions

48
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,48 @@
name: Bug report
description: Create a report to help us improve
labels:
- kind/bug
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report a bug!
If this is a security issue please report it to the [Distributions Security Mailing List](mailto:cncf-distribution-security@lists.cncf.io).
- type: textarea
id: description
attributes:
label: Description
description: Please give a clear and concise description of the bug
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduce
description: Steps to reproduce the bug
placeholder: |
1. start registry version X ...
2. `docker push image:tag` ...
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What is the expected behavior?
placeholder: |
E.g. "registry returns an incorrect API error"
- type: textarea
id: version
attributes:
label: registry version
description: Output of `registry --version`. Alternatively tell us the docker image tag.
validations:
required: true
- type: textarea
id: additional
attributes:
label: Additional Info
description: Additional info you want to provide such as logs, system info, environment, etc.
validations:
required: false

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security and Vulnerabilities
url: https://github.com/distribution/distribution/blob/main/SECURITY.md
about: Please report any security issues or vulnerabilities responsibly to the distribution maintainers team. Please do not use the public issue tracker.
- name: Questions and Discussions
url: https://github.com/distribution/distribution/discussions/new/choose
about: Use Github Discussions to ask questions and/or open discussion topics.

View file

@ -0,0 +1,12 @@
name: Feature request
description: Missing functionality? Come tell us about it!
labels:
- kind/feature
body:
- type: textarea
id: description
attributes:
label: Description
description: What is the feature you want to see?
validations:
required: true

8
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"

61
.github/labeler.yml vendored Normal file
View file

@ -0,0 +1,61 @@
area/api:
- changed-files:
- any-glob-to-any-file:
- registry/api/**
- registry/handlers/**
area/auth:
- changed-files:
- any-glob-to-any-file:
- registry/auth/**
area/build:
- changed-files:
- any-glob-to-any-file:
- Makefile
- Dockerfile
- docker-bake.hcl
- dockerfiles/**
area/cache:
- changed-files:
- any-glob-to-any-file:
- registry/storage/cache/**
area/ci:
- changed-files:
- any-glob-to-any-file:
- .github/**
- tests/**
- testutil/**
area/config:
- changed-files:
- any-glob-to-any-file:
- configuration/**
area/docs:
- changed-files:
- any-glob-to-any-file:
- README.md
- docs/**/*.md
area/proxy:
- changed-files:
- any-glob-to-any-file:
- registry/proxy/**
area/storage:
- changed-files:
- any-glob-to-any-file:
- registry/storage/**
area/storage/azure:
- changed-files:
- any-glob-to-any-file:
- registry/storage/driver/azure/**
area/storage/gcs:
- changed-files:
- any-glob-to-any-file:
- registry/storage/driver/gcs/**
area/storage/s3:
- changed-files:
- any-glob-to-any-file:
- registry/storage/driver/s3-aws/**
dependencies:
- changed-files:
- any-glob-to-any-file:
- vendor/**
- go.mod
- go.sum

View file

@ -27,18 +27,18 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
go: go:
- 1.20.12 - 1.21.8
- 1.21.5 - 1.22.1
target: target:
- test-coverage - test-coverage
- test-cloud-storage - test-cloud-storage
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v5
with: with:
go-version: ${{ matrix.go }} go-version: ${{ matrix.go }}
- -
@ -47,7 +47,7 @@ jobs:
make ${{ matrix.target }} make ${{ matrix.target }}
- -
name: Codecov name: Codecov
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
with: with:
directory: ./ directory: ./
@ -62,13 +62,13 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Docker meta name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ env.DOCKERHUB_SLUG }} ${{ env.DOCKERHUB_SLUG }}
@ -94,43 +94,53 @@ jobs:
org.opencontainers.image.description=The toolkit to pack, ship, store, and distribute container content org.opencontainers.image.description=The toolkit to pack, ship, store, and distribute container content
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- -
name: Login to DockerHub name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- -
name: Log in to GitHub Container registry name: Log in to GitHub Container registry
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- -
name: Build artifacts name: Build artifacts
uses: docker/bake-action@v2 uses: docker/bake-action@v4
with: with:
targets: artifact-all targets: artifact-all
- -
name: Move artifacts name: Rename provenance
run: |
for pdir in ./bin/*/; do
(
cd "$pdir"
binname=$(find . -name '*.tar.gz')
filename=$(basename "${binname%.tar.gz}")
mv "provenance.json" "${filename}.provenance.json"
)
done
-
name: Move and list artifacts
run: | run: |
mv ./bin/**/* ./bin/ mv ./bin/**/* ./bin/
tree -nh ./bin
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4.3.0
with: with:
name: registry name: registry
path: ./bin/* path: ./bin/*
if-no-files-found: error if-no-files-found: error
- -
name: Build image name: Build image
uses: docker/bake-action@v2 uses: docker/bake-action@v4
with: with:
files: | files: |
./docker-bake.hcl ./docker-bake.hcl
@ -145,6 +155,7 @@ jobs:
draft: true draft: true
files: | files: |
bin/*.tar.gz bin/*.tar.gz
bin/*.provenance.json
bin/*.sha256 bin/*.sha256
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -34,7 +34,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 2 fetch-depth: 2
- -
@ -44,12 +44,12 @@ jobs:
git checkout HEAD^2 git checkout HEAD^2
- -
name: Initialize CodeQL name: Initialize CodeQL
uses: github/codeql-action/init@v2 uses: github/codeql-action/init@v3.22.12
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- -
name: Autobuild name: Autobuild
uses: github/codeql-action/autobuild@v2 uses: github/codeql-action/autobuild@v3.22.12
- -
name: Perform CodeQL Analysis name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v3.22.12

View file

@ -17,12 +17,12 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Build image name: Build image
uses: docker/bake-action@v2 uses: docker/bake-action@v4
with: with:
targets: image-local targets: image-local
- -
@ -49,7 +49,7 @@ jobs:
run: mkdir -p .out/ && mv {report.html,junit.xml} .out/ run: mkdir -p .out/ && mv {report.html,junit.xml} .out/
- -
name: Upload test results name: Upload test results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4.3.0
with: with:
name: oci-test-results-${{ github.sha }} name: oci-test-results-${{ github.sha }}
path: .out/ path: .out/

View file

@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- -
name: Update Docker Hub README name: Update Docker Hub README
uses: peter-evans/dockerhub-description@v3 uses: peter-evans/dockerhub-description@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}

View file

@ -26,27 +26,26 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup Pages - name: Setup Pages
id: pages id: pages
uses: actions/configure-pages@v3 uses: actions/configure-pages@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build docs - name: Build docs
uses: docker/bake-action@v3 uses: docker/bake-action@v4
with: with:
files: | files: |
docker-bake.hcl docker-bake.hcl
targets: docs-export targets: docs-export
provenance: false
set: | set: |
*.cache-from=type=gha,scope=docs *.cache-from=type=gha,scope=docs
*.cache-to=type=gha,scope=docs,mode=max *.cache-to=type=gha,scope=docs,mode=max
env:
DOCS_BASEURL: ${{ steps.pages.outputs.base_path }}
- name: Fix permissions - name: Fix permissions
run: | run: |
chmod -c -R +rX "./build/docs" | while read line; do chmod -c -R +rX "./build/docs" | while read line; do
echo "::warning title=Invalid file permissions automatically fixed::$line" echo "::warning title=Invalid file permissions automatically fixed::$line"
done done
- name: Upload Pages artifact - name: Upload Pages artifact
uses: actions/upload-pages-artifact@v2 uses: actions/upload-pages-artifact@v3
with: with:
path: ./build/docs path: ./build/docs
@ -70,4 +69,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@v2 # or the latest "vX.X.X" version tag for this action uses: actions/deploy-pages@v4 # or the latest "vX.X.X" version tag for this action

View file

@ -20,12 +20,12 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- -
name: Build image name: Build image
uses: docker/bake-action@v2 uses: docker/bake-action@v4
with: with:
targets: image-local targets: image-local
- -
@ -42,7 +42,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- -

View file

@ -17,9 +17,9 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Run FOSSA scan and upload build data - name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@v2 uses: fossa-contrib/fossa-action@v3
with: with:
fossa-api-key: cac3dc8d4f2ba86142f6c0f2199a160f fossa-api-key: cac3dc8d4f2ba86142f6c0f2199a160f

19
.github/workflows/label.yaml vendored Normal file
View file

@ -0,0 +1,19 @@
name: Pull Request Labeler
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request_target:
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
dot: true

View file

@ -22,12 +22,12 @@ jobs:
steps: steps:
- name: "Checkout code" - name: "Checkout code"
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1
with: with:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # tag=v2.0.6 uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # tag=v2.3.1
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -46,7 +46,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # tag=v3.0.0 uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # tag=v4.3.0
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif
@ -54,7 +54,7 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5f532563584d71fdef14ee64d17bafb34f751ce5 # tag=v1.0.26 uses: github/codeql-action/upload-sarif@1500a131381b66de0c52ac28abb13cd79f4b7ecc # tag=v2.22.12
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -29,7 +29,7 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- -
name: Run name: Run
run: | run: |

View file

@ -6,7 +6,7 @@ linters:
- goimports - goimports
- revive - revive
- ineffassign - ineffassign
- vet - govet
- unused - unused
- misspell - misspell
- bodyclose - bodyclose
@ -22,7 +22,7 @@ linters-settings:
- name: unused-parameter - name: unused-parameter
disabled: true disabled: true
run: issues:
deadline: 2m deadline: 2m
skip-dirs: exlude-dirs:
- vendor - vendor

224
.mailmap
View file

@ -1,32 +1,194 @@
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@users.noreply.github.com> Aaron Lehmann <alehmann@netflix.com>
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com> Aaron Lehmann <alehmann@netflix.com> <aaron.lehmann@docker.com>
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
Brian Bland <brian.bland@docker.com> Brian Bland <brian.t.bland@gmail.com> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu> Alexander Morozov <lk4d4math@gmail.com>
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com> Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com> Anders Ingemann <aim@orbit.online>
Andrew Meredith <andymeredith@gmail.com> Andrew Meredith <kendru@users.noreply.github.com> Andrew Meredith <andymeredith@gmail.com>
harche <p.harshal@gmail.com> harche <harche@users.noreply.github.com> Andrew Meredith <andymeredith@gmail.com> <kendru@users.noreply.github.com>
Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com> Andrey Smirnov <andrey.smirnov@siderolabs.com>
Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com> Andrii Soldatenko <andrii.soldatenko@gmail.com>
Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com> Andrii Soldatenko <andrii.soldatenko@gmail.com> <andrii.soldatenko@dynatrace.com>
Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr> Anthony Ramahay <thewolt@gmail.com>
davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com> Antonio Murdaca <antonio.murdaca@gmail.com>
Omer Cohen <git@omer.io> Omer Cohen <git@omerc.net> Antonio Murdaca <antonio.murdaca@gmail.com> <amurdaca@redhat.com>
Eric Yang <windfarer@gmail.com> Eric Yang <Windfarer@users.noreply.github.com> Antonio Murdaca <antonio.murdaca@gmail.com> <me@runcom.ninja>
Nikita Tarasov <nikita@mygento.ru> Nikita <luckyraul@users.noreply.github.com> Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@linux.com>
Yu Wang <yuwa@microsoft.com> yuwaMSFT2 <yuwa@microsoft.com> Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@redhat.com>
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@users.noreply.github.com>
Austin Vazquez <macedonv@amazon.com>
Benjamin Schanzel <benjamin.schanzel@bmw.de>
Brian Bland <brian.t.bland@gmail.com>
Brian Bland <brian.t.bland@gmail.com> <brian.bland@docker.com>
Brian Bland <brian.t.bland@gmail.com> <r4nd0m1n4t0r@gmail.com>
Chad Faragher <wyckster@hotmail.com>
Cory Snider <csnider@mirantis.com>
CrazyMax <github@crazymax.dev>
CrazyMax <github@crazymax.dev> <1951866+crazy-max@users.noreply.github.com>
CrazyMax <github@crazymax.dev> <crazy-max@users.noreply.github.com>
Cristian Staretu <cristian.staretu@gmail.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejack@users.noreply.github.com>
Cristian Staretu <cristian.staretu@gmail.com> <unclejacksons@gmail.com>
Daniel Nephin <dnephin@gmail.com>
Daniel Nephin <dnephin@gmail.com> <dnephin@docker.com>
David Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David Wu <dwu7401@gmail.com>
David Wu <dwu7401@gmail.com> <david.wu@docker.com>
Derek McGowan <derek@mcg.dev>
Derek McGowan <derek@mcg.dev> <derek@mcgstyle.net>
Dimitar Kostadinov <dimitar.kostadinov@sap.com>
Doug Davis <dug@us.ibm.com>
Doug Davis <dug@us.ibm.com> <duglin@users.noreply.github.com>
Emmanuel Ferdman <emmanuelferdman@gmail.com>
Eng Zer Jun <engzerjun@gmail.com>
Eric Yang <windfarer@gmail.com>
Eric Yang <windfarer@gmail.com> <Windfarer@users.noreply.github.com>
Eric Yang <windfarer@gmail.com> <qizhao.yang@daocloud.io>
Erica Windisch <erica@windisch.us>
Erica Windisch <erica@windisch.us> <eric@windisch.us>
Guillaume J. Charmes <charmes.guillaume@gmail.com>
Guillaume J. Charmes <charmes.guillaume@gmail.com> <guillaume.charmes@dotcloud.com>
Guillaume J. Charmes <charmes.guillaume@gmail.com> <guillaume@charmes.net>
Guillaume J. Charmes <charmes.guillaume@gmail.com> <guillaume@docker.com>
Guillaume J. Charmes <charmes.guillaume@gmail.com> <guillaume@dotcloud.com>
Hayley Swimelar <hswimelar@gmail.com>
Ismail Alidzhikov <i.alidjikov@gmail.com>
Jaime Martinez <jmartinez@gitlab.com>
James Hewitt <james.hewitt@uk.ibm.com>
Jessica Frazelle <jess@oxide.computer>
Jessica Frazelle <jess@oxide.computer> <acidburn@docker.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@google.com>
Jessica Frazelle <jess@oxide.computer> <acidburn@microsoft.com>
Jessica Frazelle <jess@oxide.computer> <jess@docker.com>
Jessica Frazelle <jess@oxide.computer> <jess@mesosphere.com>
Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
Joao Fernandes <joaofnfernandes@gmail.com>
Joao Fernandes <joaofnfernandes@gmail.com> <joao.fernandes@docker.com>
João Pereira <484633+joaodrp@users.noreply.github.com>
Joffrey F <joffrey@docker.com>
Joffrey F <joffrey@docker.com> <f.joffrey@gmail.com>
Joffrey F <joffrey@docker.com> <joffrey@dotcloud.com>
Johan Euphrosine <proppy@google.com>
Johan Euphrosine <proppy@google.com> <proppy@aminche.com>
John Howard <github@lowenna.com>
John Howard <github@lowenna.com> <jhoward@microsoft.com>
Josh Hawn <jlhawn@berkeley.edu>
Josh Hawn <jlhawn@berkeley.edu> <josh.hawn@docker.com>
Joyce Brum <joycebrumu.u@gmail.com>
Joyce Brum <joycebrumu.u@gmail.com> <joycebrum@google.com>
Justin Cormack <justin.cormack@docker.com>
Justin Cormack <justin.cormack@docker.com> <justin.cormack@unikernel.com>
Justin Cormack <justin.cormack@docker.com> <justin@specialbusservice.com>
Kirat Singh <kirat.singh@gmail.com>
Kirat Singh <kirat.singh@gmail.com> <kirat.singh@beacon.io>
Kirat Singh <kirat.singh@gmail.com> <kirat.singh@wsq.io>
Kyle Squizzato <ksquizz@gmail.com>
Liang Zheng <zhengliang0901@gmail.com>
Luca Bruno <lucab@debian.org>
Luca Bruno <lucab@debian.org> <luca.bruno@coreos.com>
Mahmoud Kandil <47168819+MahmoudKKandil@users.noreply.github.com>
Manish Tomar <manish.tomar@docker.com>
Manish Tomar <manish.tomar@docker.com> <manishtomar@users.noreply.github.com>
Maria Bermudez <bermudez.mt@gmail.com>
Maria Bermudez <bermudez.mt@gmail.com> <bermudezmt@users.noreply.github.com>
Markus Thömmes <markusthoemmes@me.com>
Matt Linville <matt@linville.me>
Matt Linville <matt@linville.me> <misty@apache.org>
Matt Linville <matt@linville.me> <misty@docker.com>
Michael Crosby <crosbymichael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <crosby.michael@gmail.com>
Michael Crosby <crosbymichael@gmail.com> <michael@crosbymichael.com>
Michael Crosby <crosbymichael@gmail.com> <michael@docker.com>
Michael Crosby <crosbymichael@gmail.com> <michael@thepasture.io>
Michal Minar <miminar@redhat.com>
Michal Minar <miminar@redhat.com> Michal Minář <miminar@redhat.com>
Mike Brown <brownwm@us.ibm.com>
Mike Brown <brownwm@us.ibm.com> <mikebrow@users.noreply.github.com>
Mikel Rychliski <mikel@mikelr.com>
Milos Gajdos <milosthegajdos@gmail.com>
Milos Gajdos <milosthegajdos@gmail.com> <1392526+milosgajdos@users.noreply.github.com>
Milos Gajdos <milosthegajdos@gmail.com> <milosgajdos83@gmail.com>
Nikita Tarasov <nikita@mygento.ru>
Nikita Tarasov <nikita@mygento.ru> <luckyraul@users.noreply.github.com>
Oleg Bulatov <oleg@bulatov.me>
Oleg Bulatov <oleg@bulatov.me> <obulatov@redhat.com>
Olivier Gambier <olivier@docker.com>
Olivier Gambier <olivier@docker.com> <dmp42@users.noreply.github.com>
Omer Cohen <git@omer.io>
Omer Cohen <git@omer.io> <git@omerc.net>
Paul Meyer <49727155+katexochen@users.noreply.github.com>
Per Lundberg <perlun@gmail.com>
Per Lundberg <perlun@gmail.com> <per.lundberg@ecraft.com>
Peter Dave Hello <hsu@peterdavehello.org>
Peter Dave Hello <hsu@peterdavehello.org> <PeterDaveHello@users.noreply.github.com>
Phil Estes <estesp@gmail.com>
Phil Estes <estesp@gmail.com> <estesp@amazon.com>
Phil Estes <estesp@gmail.com> <estesp@linux.vnet.ibm.com>
Richard Scothern <richard.scothern@gmail.com>
Richard Scothern <richard.scothern@gmail.com> <richard.scothern@docker.com>
Rober Morales-Chaparro <rober.morales@rstor.io>
Rober Morales-Chaparro <rober.morales@rstor.io> <rober@rstor.io>
Robin Ketelbuters <robin.ketelbuters@gmail.com>
Sebastiaan van Stijn <github@gone.nl>
Sebastiaan van Stijn <github@gone.nl> <moby@example.com>
Sebastiaan van Stijn <github@gone.nl> <sebastiaan@ws-key-sebas3.dpi1.dpi>
Sebastiaan van Stijn <github@gone.nl> <thaJeztah@users.noreply.github.com>
Sharif Nassar <sharif@mrwacky.com>
Sharif Nassar <sharif@mrwacky.com> <mrwacky42@users.noreply.github.com>
Solomon Hykes <solomon@dagger.io>
Solomon Hykes <solomon@dagger.io> <s@docker.com>
Solomon Hykes <solomon@dagger.io> <solomon.hykes@dotcloud.com>
Solomon Hykes <solomon@dagger.io> <solomon@docker.com>
Solomon Hykes <solomon@dagger.io> <solomon@dotcloud.com>
Stephen Day <stevvooe@gmail.com>
Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
Steven Kalt <SKalt@users.noreply.github.com>
Sven Dowideit <SvenDowideit@home.org.au>
Sven Dowideit <SvenDowideit@home.org.au> <SvenDowideit@users.noreply.github.com>
Sylvain DESGRAIS <sylvain.desgrais@gmail.com>
Tadeusz Dudkiewicz <tadeusz.dudkiewicz@rtbhouse.com>
Tibor Vass <teabee89@gmail.com>
Tibor Vass <teabee89@gmail.com> <tibor@docker.com>
Tibor Vass <teabee89@gmail.com> <tiborvass@users.noreply.github.com>
Victor Vieux <victorvieux@gmail.com>
Victor Vieux <victorvieux@gmail.com> <dev@vvieux.com>
Victor Vieux <victorvieux@gmail.com> <victor.vieux@docker.com>
Victor Vieux <victorvieux@gmail.com> <victor.vieux@dotcloud.com>
Victor Vieux <victorvieux@gmail.com> <victor@docker.com>
Victor Vieux <victorvieux@gmail.com> <victor@dotcloud.com>
Victor Vieux <victorvieux@gmail.com> <victorvieux@gmail.com>
Victor Vieux <victorvieux@gmail.com> <vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com>
Victoria Bialas <victoria.bialas@docker.com> <londoncalling@users.noreply.github.com>
Vincent Batts <vbatts@redhat.com>
Vincent Batts <vbatts@redhat.com> <vbatts@hashbangbash.com>
Vincent Demeester <vincent.demeester@docker.com>
Vincent Demeester <vincent.demeester@docker.com> <vincent+github@demeester.fr>
Vincent Demeester <vincent.demeester@docker.com> <vincent@demeester.fr>
Vincent Demeester <vincent.demeester@docker.com> <vincent@sbr.pm>
Vincent Giersch <vincent@giersch.fr>
Vincent Giersch <vincent@giersch.fr> <vincent.giersch@ovh.net>
Wang Yan <wangyan@vmware.com>
Wen-Quan Li <legendarilylwq@gmail.com>
Wen-Quan Li <legendarilylwq@gmail.com> <wenquan.li@hp.com>
Wen-Quan Li <legendarilylwq@gmail.com> <wenquan.li@hpe.com>
Yu Wang <yuwa@microsoft.com>
Yu Wang <yuwa@microsoft.com> Yu Wang (UC) <yuwa@microsoft.com> Yu Wang <yuwa@microsoft.com> Yu Wang (UC) <yuwa@microsoft.com>
Olivier Gambier <olivier@docker.com> dmp <dmp@loaner.local> baojiangnan <baojiangnan@meituan.com>
Olivier Gambier <olivier@docker.com> Olivier <o+github@gambier.email> baojiangnan <baojiangnan@meituan.com> <baojn1998@163.com>
Olivier Gambier <olivier@docker.com> Olivier <dmp42@users.noreply.github.com> erezrokah <erezrokah@users.noreply.github.com>
Elsan Li 李楠 <elsanli@tencent.com> elsanli(李楠) <elsanli@tencent.com> goodactive <goodactive@qq.com>
Rui Cao <ruicao@alauda.io> ruicao <ruicao@alauda.io> gotgelf <gotgelf@gmail.com>
Gwendolynne Barr <gwendolynne.barr@docker.com> gbarr01 <gwendolynne.barr@docker.com> guoguangwu <guoguangwug@gmail.com>
Haibing Zhou 周海兵 <zhouhaibing089@gmail.com> zhouhaibing089 <zhouhaibing089@gmail.com> harche <p.harshal@gmail.com>
Feng Honglin <tifayuki@gmail.com> tifayuki <tifayuki@gmail.com> harche <p.harshal@gmail.com> <harche@users.noreply.github.com>
Helen Xie <xieyulin821@harmonycloud.cn> Helen-xie <xieyulin821@harmonycloud.cn> icefed <zlwangel@gmail.com>
Mike Brown <brownwm@us.ibm.com> Mike Brown <mikebrow@users.noreply.github.com> oliver-goetz <o.goetz@sap.com>
Manish Tomar <manish.tomar@docker.com> Manish Tomar <manishtomar@users.noreply.github.com> xiaoxiangxianzi <zhaoyizheng@outlook.com>
Sakeven Jiang <jc5930@sina.cn> sakeven <jc5930@sina.cn>

530
AUTHORS Normal file
View file

@ -0,0 +1,530 @@
# This file lists all individuals having contributed content to the repository.
# For how it is generated, see dockerfiles/authors.Dockerfile.
a-palchikov <deemok@gmail.com>
Aaron Lehmann <alehmann@netflix.com>
Aaron Schlesinger <aschlesinger@deis.com>
Aaron Vinson <avinson.public@gmail.com>
Adam Dobrawy <ad-m@users.noreply.github.com>
Adam Duke <adam.v.duke@gmail.com>
Adam Enger <adamenger@gmail.com>
Adam Kaplan <adam.kaplan@redhat.com>
Adam Wolfe Gordon <awg@digitalocean.com>
AdamKorcz <adam@adalogics.com>
Adrian Mouat <adrian.mouat@gmail.com>
Adrian Plata <adrian.plata@docker.com>
Adrien Duermael <adrien@duermael.com>
Ahmet Alp Balkan <ahmetalpbalkan@gmail.com>
Aidan Hobson Sayers <aidanhs@cantab.net>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Aleksejs Sinicins <monder@monder.cc>
Alex <aleksandrosansan@gmail.com>
Alex Chan <alex.chan@metaswitch.com>
Alex Elman <aelman@indeed.com>
Alex Laties <agl@tumblr.com>
Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com>
Alexey Gladkov <gladkov.alexey@gmail.com>
Alfonso Acosta <fons@syntacticsugar.consulting>
allencloud <allen.sun@daocloud.io>
Alvin Feng <alvin4feng@yahoo.com>
amitshukla <ashukla73@hotmail.com>
Amy Lindburg <amy.lindburg@docker.com>
Andreas Hassing <andreas@famhassing.dk>
Andrew Bulford <andrew.bulford@redmatter.com>
Andrew Hsu <andrewhsu@acm.org>
Andrew Lavery <laverya@umich.edu>
Andrew Leung <anwleung@gmail.com>
Andrew Lively <andrew.lively2@gmail.com>
Andrew Meredith <andymeredith@gmail.com>
Andrew T Nguyen <andrew.nguyen@docker.com>
Andrews Medina <andrewsmedina@gmail.com>
Andrey Kostov <kostov.andrey@gmail.com>
Andrii Soldatenko <andrii.soldatenko@gmail.com>
Andy Goldstein <agoldste@redhat.com>
andyzhangx <xiazhang@microsoft.com>
Anian Z <ziegler@sicony.de>
Anil Belur <askb23@gmail.com>
Anis Elleuch <vadmeste@gmail.com>
Ankush Agarwal <ankushagarwal11@gmail.com>
Anne Henmi <41210220+ahh-docker@users.noreply.github.com>
Anton Tiurin <noxiouz@yandex.ru>
Antonio Mercado <amercado@thinknode.com>
Antonio Murdaca <antonio.murdaca@gmail.com>
Antonio Ojea <antonio.ojea.garcia@gmail.com>
Anusha Ragunathan <anusha@docker.com>
Arien Holthuizen <aholthuizen@schubergphilis.com>
Arko Dasgupta <arkodg@users.noreply.github.com>
Arnaud Porterie <arnaud.porterie@docker.com>
Arthur Baars <arthur@semmle.com>
Arthur Gautier <baloo@gandi.net>
Asuka Suzuki <hello@tanksuzuki.com>
Avi Miller <avi.miller@oracle.com>
Aviral Takkar <aviral26@users.noreply.github.com>
Ayose Cazorla <ayosec@gmail.com>
BadZen <dave.trombley@gmail.com>
baojiangnan <baojiangnan@meituan.com>
Ben Bodenmiller <bbodenmiller@hotmail.com>
Ben De St Paer-Gotch <bende@outlook.com>
Ben Emamian <ben@ictace.com>
Ben Firshman <ben@firshman.co.uk>
Ben Kochie <superq@gmail.com>
Ben Manuel <ben.manuel@procore.com>
Bhavin Gandhi <bhavin192@users.noreply.github.com>
Bill <NonCreature0714@users.noreply.github.com>
bin liu <liubin0329@gmail.com>
Bouke van der Bijl <me@bou.ke>
Bracken Dawson <abdawson@gmail.com>
Brandon Mitchell <git@bmitch.net>
Brandon Philips <brandon@ifup.co>
Brett Higgins <brhiggins@arbor.net>
Brian Bland <brian.t.bland@gmail.com>
Brian Goff <cpuguy83@gmail.com>
burnettk <burnettk@gmail.com>
Caleb Spare <cespare@gmail.com>
Carson A <ca@carsonoid.net>
Cezar Sa Espinola <cezarsa@gmail.com>
Chad Faragher <wyckster@hotmail.com>
Chaos John <chaosjohn.yjh@icloud.com>
Charles Smith <charles.smith@docker.com>
Cheng Zheng <chengzheng.apply@gmail.com>
chlins <chenyuzh@vmware.com>
Chris Aniszczyk <caniszczyk@gmail.com>
Chris Dillon <squarism@gmail.com>
Chris K. Wong <chriskw.xyz@gmail.com>
Chris Patterson <chrispat@github.com>
Christopher Yeleighton <ne01026@shark.2a.pl>
Christy Perez <christy@linux.vnet.ibm.com>
Chuanying Du <cydu@google.com>
Clayton Coleman <ccoleman@redhat.com>
Collin Shoop <cshoop@digitalocean.com>
Corey Quon <corey.quon@gmail.com>
Cory Snider <csnider@mirantis.com>
CrazyMax <github@crazymax.dev>
cressie176 <github@stephen-cresswell.net>
Cristian Staretu <cristian.staretu@gmail.com>
cui fliter <imcusg@gmail.com>
cuiwei13 <cuiwei13@pku.edu.cn>
cyli <cyli@twistedmatrix.com>
Daehyeok Mun <daehyeok@gmail.com>
Daisuke Fujita <dtanshi45@gmail.com>
Damien Mathieu <dmathieu@salesforce.com>
Dan Fredell <furtchet@gmail.com>
Dan Walsh <dwalsh@redhat.com>
Daniel Helfand <helfand.4@gmail.com>
Daniel Huhn <daniel@danielhuhn.de>
Daniel Menet <membership@sontags.ch>
Daniel Mizyrycki <mzdaniel@glidelink.net>
Daniel Nephin <dnephin@gmail.com>
Daniel, Dao Quang Minh <dqminh89@gmail.com>
Danila Fominykh <dancheg97@fmnx.su>
Darren Shepherd <darren@rancher.com>
Dave <david.warshaw@gmail.com>
Dave Trombley <dave.trombley@gmail.com>
Dave Tucker <dt@docker.com>
David Calavera <david.calavera@gmail.com>
David Justice <david@devigned.com>
David Karlsson <david.karlsson@docker.com>
David Lawrence <david.lawrence@docker.com>
David Luu <david@davidluu.info>
David Mackey <tdmackey@booleanhaiku.com>
David van der Spek <vanderspek.david@gmail.com>
David Verhasselt <david@crowdway.com>
David Wu <dwu7401@gmail.com>
David Xia <dxia@spotify.com>
Dawn W Docker <dawn.wood@users.noreply.github.com>
ddelange <14880945+ddelange@users.noreply.github.com>
Dejan Golja <dejan@golja.org>
Denis Andrejew <da.colonel@gmail.com>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek <crq@kernel.org>
Derek McGowan <derek@mcg.dev>
Deshi Xiao <xiaods@gmail.com>
Dimitar Kostadinov <dimitar.kostadinov@sap.com>
Diogo Mónica <diogo.monica@gmail.com>
DJ Enriquez <dj.enriquez@infospace.com>
Djibril Koné <kone.djibril@gmail.com>
dmp <dmp@loaner.local>
Don Bowman <don@agilicus.com>
Don Kjer <don.kjer@gmail.com>
Donald Huang <don.hcd@gmail.com>
Doug Davis <dug@us.ibm.com>
drornir <drornir@users.noreply.github.com>
duanhongyi <duanhongyi@doopai.com>
ducksecops <daniel@ducksecops.uk>
E. M. Bray <erik.m.bray@gmail.com>
Edgar Lee <edgar.lee@docker.com>
Elliot Pahl <elliot.pahl@gmail.com>
elsanli(李楠) <elsanli@tencent.com>
Elton Stoneman <elton@sixeyed.com>
Emmanuel Briney <emmanuel.briney@docker.com>
Eng Zer Jun <engzerjun@gmail.com>
Eohyung Lee <liquidnuker@gmail.com>
Eric Yang <windfarer@gmail.com>
Erica Windisch <erica@windisch.us>
Erik Hollensbe <github@hollensbe.org>
Etki <etki@etki.me>
Eugene Lubarsky <eug48@users.noreply.github.com>
eyjhb <eyjhbb@gmail.com>
eyjhbb@gmail.com <eyjhbb@gmail.com>
Fabio Berchtold <jamesclonk@jamesclonk.ch>
Fabio Falci <fabiofalci@gmail.com>
Fabio Huser <fabio@fh1.ch>
farmerworking <farmerworking@gmail.com>
fate-grand-order <chenjg@harmonycloud.cn>
Felix Bünemann <buenemann@louis.info>
Felix Yan <felixonmars@archlinux.org>
Feng Honglin <tifayuki@gmail.com>
Fernando Mayo Fernandez <fernando@undefinedlabs.com>
Flavian Missi <fmissi@redhat.com>
Florentin Raud <florentin.raud@gmail.com>
forkbomber <forkbomber@users.noreply.github.com>
Frank Chen <frankchn@gmail.com>
Frederick F. Kautz IV <fkautz@alumni.cmu.edu>
Gabor Nagy <mail@aigeruth.hu>
gabriell nascimento <gabriell@bluesoft.com.br>
Gaetan <gdevillele@gmail.com>
gary schaetz <gary@schaetzkc.com>
gbarr01 <gwendolynne.barr@docker.com>
Geoffrey Hausheer <rc2012@pblue.org>
ghodsizadeh <mehdi.ghodsizadeh@gmail.com>
Giovanni Toraldo <giovanni.toraldo@eng.it>
Gladkov Alexey <agladkov@redhat.com>
Gleb M Borisov <borisov.gleb@gmail.com>
Gleb Schukin <gschukin@ptsecurity.com>
glefloch <glfloch@gmail.com>
Glyn Owen Hanmer <1295698+glynternet@users.noreply.github.com>
gotgelf <gotgelf@gmail.com>
Grachev Mikhail <work@mgrachev.com>
Grant Watters <grant.watters@docker.com>
Greg Rebholz <gregrebholz@gmail.com>
Guillaume J. Charmes <charmes.guillaume@gmail.com>
Guillaume Rose <guillaume.rose@docker.com>
Gábor Lipták <gliptak@gmail.com>
harche <p.harshal@gmail.com>
hasheddan <georgedanielmangum@gmail.com>
Hayley Swimelar <hswimelar@gmail.com>
Helen-xie <xieyulin821@harmonycloud.cn>
Henri Gomez <henri.gomez@gmail.com>
Honglin Feng <tifayuki@gmail.com>
Hu Keping <hukeping@huawei.com>
Hua Wang <wanghua.humble@gmail.com>
HuKeping <hukeping@huawei.com>
Huu Nguyen <whoshuu@gmail.com>
ialidzhikov <i.alidjikov@gmail.com>
Ian Babrou <ibobrik@gmail.com>
iasoon <ilion.beyst@gmail.com>
igayoso <igayoso@gmail.com>
Igor Dolzhikov <bluesriverz@gmail.com>
Igor Morozov <igmorv@gmail.com>
Ihor Dvoretskyi <ihor@linux.com>
Ilion Beyst <ilion.beyst@gmail.com>
Ina Panova <ipanova@redhat.com>
Irene Diez <idiez@redhat.com>
Ismail Alidzhikov <i.alidjikov@gmail.com>
Jack Baines <jack.baines@uk.ibm.com>
Jack Griffin <jackpg14@gmail.com>
Jacob Atzen <jatzen@gmail.com>
Jake Moshenko <jake@devtable.com>
Jakob Ackermann <das7pad@outlook.com>
Jakub Mikulas <jakub@mikul.as>
James Findley <jfindley@fastmail.com>
James Hewitt <james.hewitt@uk.ibm.com>
James Lal <james@lightsofapollo.com>
Jason Freidman <jason.freidman@gmail.com>
Jason Heiss <jheiss@aput.net>
Javier Palomo Almena <javier.palomo.almena@gmail.com>
jdolitsky <393494+jdolitsky@users.noreply.github.com>
Jeff Nickoloff <jeff@allingeek.com>
Jeffrey van Gogh <jvg@google.com>
jerae-duffin <83294991+jerae-duffin@users.noreply.github.com>
Jeremy THERIN <jtherin@scaleway.com>
Jesse Brown <jabrown85@gmail.com>
Jesse Haka <haka.jesse@gmail.com>
Jessica Frazelle <jess@oxide.computer>
jhaohai <jhaohai@foxmail.com>
Jianqing Wang <tsing@jianqing.org>
Jihoon Chung <jihoon@gmail.com>
Jim Galasyn <jim.galasyn@docker.com>
Joao Fernandes <joaofnfernandes@gmail.com>
Joffrey F <joffrey@docker.com>
Johan Euphrosine <proppy@google.com>
John Howard <github@lowenna.com>
John Mulhausen <john@docker.com>
John Starks <jostarks@microsoft.com>
Jon Johnson <jonjohnson@google.com>
Jon Poler <jonathan.poler@apcera.com>
Jonas Hecht <jonas.hecht@codecentric.de>
Jonathan Boulle <jonathanboulle@gmail.com>
Jonathan Lee <jonjohn1232009@gmail.com>
Jonathan Rudenberg <jonathan@titanous.com>
Jordan Liggitt <jliggitt@redhat.com>
Jose D. Gomez R <jose.gomez@suse.com>
Josh Chorlton <josh.chorlton@docker.com>
Josh Dolitsky <josh@dolit.ski>
Josh Hawn <jlhawn@berkeley.edu>
Josiah Kiehl <jkiehl@riotgames.com>
Joyce Brum <joycebrumu.u@gmail.com>
João Pereira <484633+joaodrp@users.noreply.github.com>
Julien Bordellier <1444415+jstoja@users.noreply.github.com>
Julien Fernandez <julien.fernandez@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Cormack <justin.cormack@docker.com>
Justin I. Nevill <JustinINevill@users.noreply.github.com>
Justin Santa Barbara <justin@fathomdb.com>
kaiwentan <kaiwentan@harmonycloud.cn>
Ke Xu <leonhartx.k@gmail.com>
Keerthan Mala <kmala@engineyard.com>
Kelsey Hightower <kelsey.hightower@gmail.com>
Ken Cochrane <KenCochrane@gmail.com>
Kenneth Lim <kennethlimcp@gmail.com>
Kenny Leung <kleung@google.com>
Kevin Lin <kevin@kelda.io>
Kevin Robatel <kevinrob2@gmail.com>
Kira <me@imkira.com>
Kirat Singh <kirat.singh@gmail.com>
L-Hudson <44844738+L-Hudson@users.noreply.github.com>
Lachlan Cooper <lachlancooper@gmail.com>
Laura Brehm <laurabrehm@hey.com>
Lei Jitang <leijitang@huawei.com>
Lenny Linux <tippexs91@googlemail.com>
Leonardo Azize Martins <lazize@users.noreply.github.com>
leonstrand <leonstrand@gmail.com>
Li Yi <denverdino@gmail.com>
Liam White <liamwhite@uk.ibm.com>
libo.huang <huanglibo2010@gmail.com>
LingFaKe <lingfake@huawei.com>
Liron Levin <liron@twistlock.com>
lisong <lisong@cdsunrise.net>
Littlemoon917 <18084421+Littlemoon917@users.noreply.github.com>
Liu Hua <sdu.liu@huawei.com>
liuchang0812 <liuchang0812@gmail.com>
liyongxin <yxli@alauda.io>
Lloyd Ramey <lnr0626@gmail.com>
lostsquirrel <lostsquirreli@hotmail.com>
Louis Kottmann <louis.kottmann@gmail.com>
Luca Bruno <lucab@debian.org>
Lucas França de Oliveira <lucasfdo@palantir.com>
Lucas Santos <lhs.santoss@gmail.com>
Luis Lobo Borobia <luislobo@gmail.com>
Luke Carpenter <x@rubynerd.net>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
Makoto Oda <truth_jp_4133@yahoo.co.jp>
mallchin <mallchin@mac.com>
Manish Tomar <manish.tomar@docker.com>
Marco Hennings <marco.hennings@freiheit.com>
Marcus Martins <marcus@docker.com>
Maria Bermudez <bermudez.mt@gmail.com>
Mark Sagi-Kazar <mark.sagikazar@gmail.com>
Mary Anthony <mary@docker.com>
Masataka Mizukoshi <m.mizukoshi.wakuwaku@gmail.com>
Matin Rahmanian <itsmatinx@gmail.com>
MATSUMOTO TAKEAKI <takeaki.matsumoto@linecorp.com>
Matt Bentley <mbentley@mbentley.net>
Matt Duch <matt@learnmetrics.com>
Matt Linville <matt@linville.me>
Matt Moore <mattmoor@google.com>
Matt Robenolt <matt@ydekproductions.com>
Matt Tescher <matthew.tescher@docker.com>
Matthew Balvanz <matthew.balvanz@workiva.com>
Matthew Green <greenmr@live.co.uk>
Matthew Riley <mattdr@google.com>
Maurice Sotzny <ailuridae@users.noreply.github.com>
Meaglith Ma <genedna@gmail.com>
Michael Bonfils <bonfils.michael@protonmail.com>
Michael Crosby <crosbymichael@gmail.com>
Michael Prokop <mika@grml.org>
Michael Vetter <jubalh@iodoru.org>
Michal Fojtik <mfojtik@redhat.com>
Michal Gebauer <mishak@mishak.net>
Michal Guerquin <michalg@allenai.org>
Michal Minar <miminar@redhat.com>
Mike Brown <brownwm@us.ibm.com>
Mike Lundy <mike@fluffypenguin.org>
Mike Truman <miketruman42@gmail.com>
Milos Gajdos <milosthegajdos@gmail.com>
Miquel Sabaté <msabate@suse.com>
mlmhl <409107750@qq.com>
Monika Katiyar <monika@jeavio.com>
Morgan Bauer <mbauer@us.ibm.com>
moxiegirl <mary@docker.com>
mqliang <mqliang.zju@gmail.com>
Muesli <solom.emmanuel@gmail.com>
Nan Monnand Deng <monnand@gmail.com>
Nat Zimmermann <ntzm@users.noreply.github.com>
Nathan Sullivan <nathan@nightsys.net>
Naveed Jamil <naveed.jamil@tenpearl.com>
Neil Wilson <neil@aldur.co.uk>
nevermosby <robolwq@qq.com>
Nghia Tran <tcnghia@gmail.com>
Nicolas De Loof <nicolas.deloof@gmail.com>
Nikita Tarasov <nikita@mygento.ru>
ning xie <andy.xning@gmail.com>
Nishant Totla <nishanttotla@gmail.com>
Noah Treuhaft <noah.treuhaft@docker.com>
Novak Ivanovski <novakivanovski@gmail.com>
Nuutti Kotivuori <nuutti.kotivuori@poplatek.fi>
Nycholas de Oliveira e Oliveira <nycholas@gmail.com>
Oilbeater <liumengxinfly@gmail.com>
Oleg Bulatov <oleg@bulatov.me>
olegburov <oleg.burov@outlook.com>
Olivier <o+github@gambier.email>
Olivier Gambier <olivier@docker.com>
Olivier Jacques <olivier.jacques@hp.com>
ollypom <oppomeroy@gmail.com>
Omer Cohen <git@omer.io>
Oscar Caballero <ocaballero@opensistemas.com>
Owen W. Taylor <otaylor@fishsoup.net>
paigehargrave <Paige.hargrave@docker.com>
Parth Mehrotra <parth@mehrotra.me>
Pascal Borreli <pascal@borreli.com>
Patrick Devine <patrick.devine@docker.com>
Patrick Easters <peasters@redhat.com>
Paul Cacheux <paul.cacheux@datadoghq.com>
Pavel Antonov <ddc67cd@gmail.com>
Paweł Gronowski <pawel.gronowski@docker.com>
Per Lundberg <perlun@gmail.com>
Peter Choi <reikani@Peters-MacBook-Pro.local>
Peter Dave Hello <hsu@peterdavehello.org>
Peter Kokot <peterkokot@gmail.com>
Phil Estes <estesp@gmail.com>
Philip Misiowiec <philip@atlashealth.com>
Pierre-Yves Ritschard <pyr@spootnik.org>
Pieter Scheffers <pieter.scheffers@gmail.com>
Qiang Huang <h.huangqiang@huawei.com>
Qiao Anran <qiaoanran@gmail.com>
Radon Rosborough <radon.neon@gmail.com>
Randy Barlow <randy@electronsweatshop.com>
Raphaël Enrici <raphael@root-42.com>
Ricardo Maraschini <ricardo.maraschini@gmail.com>
Richard Scothern <richard.scothern@gmail.com>
Rick Wieman <git@rickw.nl>
Rik Nijessen <rik@keefo.nl>
Riyaz Faizullabhoy <riyaz.faizullabhoy@docker.com>
Rober Morales-Chaparro <rober.morales@rstor.io>
Robert Kaussow <mail@geeklabor.de>
Robert Steward <speaktorob@users.noreply.github.com>
Roberto G. Hashioka <roberto.hashioka@docker.com>
Rodolfo Carvalho <rhcarvalho@gmail.com>
ROY <qqbuby@gmail.com>
Rui Cao <ruicao@alauda.io>
ruicao <ruicao@alauda.io>
Rusty Conover <rusty@luckydinosaur.com>
Ryan Abrams <rdabrams@gmail.com>
Ryan Thomas <rthomas@atlassian.com>
sakeven <jc5930@sina.cn>
Sam Alba <sam.alba@gmail.com>
Samuel Karp <skarp@amazon.com>
sangluo <sangluo@pinduoduo.com>
Santiago Torres <torresariass@gmail.com>
Sargun Dhillon <sargun@sargun.me>
sayboras <sayboras@yahoo.com>
Sean Boran <Boran@users.noreply.github.com>
Sean P. Kane <spkane00@gmail.com>
Sebastiaan van Stijn <github@gone.nl>
Sebastien Coavoux <s.coavoux@free.fr>
Serge Dubrouski <sergeyfd@gmail.com>
Sevki Hasirci <sevki@cloudflare.com>
Sharif Nassar <sharif@mrwacky.com>
Shawn Chen <chen8132@gmail.com>
Shawn Falkner-Horine <dreadpirateshawn@gmail.com>
Shawnpku <chen8132@gmail.com>
Shengjing Zhu <zhsj@debian.org>
Shiela M Parker <smp13@live.com>
Shishir Mahajan <shishir.mahajan@redhat.com>
Shreyas Karnik <karnik.shreyas@gmail.com>
Silvin Lubecki <31478878+silvin-lubecki@users.noreply.github.com>
Simon <crydotsnakegithub@gmail.com>
Simon Thulbourn <simon+github@thulbourn.com>
Simone Locci <simone.locci@eng.it>
Smasherr <soundcracker@gmail.com>
Solomon Hykes <solomon@dagger.io>
Sora Morimoto <sora@morimoto.io>
spacexnice <yaoyao.xyy@alibaba-inc.com>
Spencer Rinehart <anubis@overthemonkey.com>
srajmane <31947381+srajmane@users.noreply.github.com>
Srini Brahmaroutu <srbrahma@us.ibm.com>
Stan Hu <stanhu@gmail.com>
Stefan Lörwald <10850250+stefanloerwald@users.noreply.github.com>
Stefan Majewsky <stefan.majewsky@sap.com>
Stefan Nica <snica@suse.com>
Stefan Weil <sw@weilnetz.de>
Stephen Day <stevvooe@gmail.com>
Steve Lasker <stevenlasker@hotmail.com>
Steven Hanna <stevenhanna6@gmail.com>
Steven Kalt <SKalt@users.noreply.github.com>
Steven Taylor <steven.taylor@me.com>
stonezdj <stonezdj@gmail.com>
sun jian <cnhttpd@gmail.com>
Sungho Moon <sungho.moon@navercorp.com>
Sven Dowideit <SvenDowideit@home.org.au>
Sylvain Baubeau <sbaubeau@redhat.com>
syntaxkim <40621244+syntaxkim@users.noreply.github.com>
T N <tnir@users.noreply.github.com>
t-eimizu <t-eimizu@aim.ac>
Tariq Ibrahim <tariq181290@gmail.com>
TaylorKanper <tony_kanper@hotmail.com>
Ted Reed <ted.reed@gmail.com>
Terin Stock <terinjokes@gmail.com>
tgic <farmer1992@gmail.com>
Thomas Berger <loki@lokis-chaos.de>
Thomas Sjögren <konstruktoid@users.noreply.github.com>
Tianon Gravi <admwiggin@gmail.com>
Tibor Vass <teabee89@gmail.com>
tifayuki <tifayuki@gmail.com>
Tiger Kaovilai <tkaovila@redhat.com>
Tobias Fuhrimann <mastertinner@users.noreply.github.com>
Tobias Schwab <tobias.schwab@dynport.de>
Tom Hayward <thayward@infoblox.com>
Tom Hu <tomhu1096@gmail.com>
Tonis Tiigi <tonistiigi@gmail.com>
Tony Holdstock-Brown <tony@docker.com>
Tosone <i@tosone.cn>
Trapier Marshall <trapier@users.noreply.github.com>
Trevor Pounds <trevor.pounds@gmail.com>
Trevor Wood <Trevor.G.Wood@gmail.com>
Troels Thomsen <troels@thomsen.io>
uhayate <uhayate.gong@daocloud.io>
Usha Mandya <47779042+usha-mandya@users.noreply.github.com>
Usha Mandya <usha.mandya@docker.com>
Vaidas Jablonskis <jablonskis@gmail.com>
Vega Chou <VegeChou@users.noreply.github.com>
Veres Lajos <vlajos@gmail.com>
Victor Vieux <victorvieux@gmail.com>
Victoria Bialas <victoria.bialas@docker.com>
Vidar <vl@ez.no>
Viktor Stanchev <me@viktorstanchev.com>
Vincent Batts <vbatts@redhat.com>
Vincent Demeester <vincent.demeester@docker.com>
Vincent Giersch <vincent@giersch.fr>
Vishesh Jindal <vishesh92@gmail.com>
W. Trevor King <wking@tremily.us>
Wang Jie <wangjie5@chinaskycloud.com>
Wang Yan <wangyan@vmware.com>
Wassim Dhif <wassimdhif@gmail.com>
wayne <wayne.warren.s@gmail.com>
Wei Fu <fuweid89@gmail.com>
Wei Meng <wemeng@microsoft.com>
weiyuan.yl <weiyuan.yl@alibaba-inc.com>
Wen-Quan Li <legendarilylwq@gmail.com>
Wenkai Yin <yinw@vmware.com>
william wei <1342247033@qq.com>
xg.song <xg.song@venusource.com>
xiekeyang <xiekeyang@huawei.com>
Xueshan Feng <xueshan.feng@gmail.com>
Yann ROBERT <yann.robert@anantaplex.fr>
Yannick Fricke <YannickFricke@users.noreply.github.com>
yaoyao.xyy <yaoyao.xyy@alibaba-inc.com>
yixi zhang <yixi@memsql.com>
Yong Tang <yong.tang.github@outlook.com>
Yong Wen Chua <lawliet89@users.noreply.github.com>
Yongxin Li <yxli@alauda.io>
Yu Wang <yuwa@microsoft.com>
yuexiao-wang <wang.yuexiao@zte.com.cn>
YuJie <390282283@qq.com>
yuzou <zouyu7@huawei.com>
Zhang Wei <zhangwei555@huawei.com>
zhipengzuo <zuozhipeng@baidu.com>
zhouhaibing089 <zhouhaibing089@gmail.com>
zounengren <zounengren@cmss.chinamobile.com>
姜继忠 <jizhong.jiangjz@alibaba-inc.com>

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.21.5 ARG GO_VERSION=1.22.4
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
ARG XX_VERSION=1.2.1 ARG XX_VERSION=1.2.1
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
@ -16,7 +16,7 @@ FROM base AS version
ARG PKG=github.com/distribution/distribution/v3 ARG PKG=github.com/distribution/distribution/v3
RUN --mount=target=. \ RUN --mount=target=. \
VERSION=$(git describe --match 'v[0-9]*' --dirty='.m' --always --tags) REVISION=$(git rev-parse HEAD)$(if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi); \ VERSION=$(git describe --match 'v[0-9]*' --dirty='.m' --always --tags) REVISION=$(git rev-parse HEAD)$(if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi); \
echo "-X ${PKG}/version.Version=${VERSION#v} -X ${PKG}/version.Revision=${REVISION} -X ${PKG}/version.Package=${PKG}" | tee /tmp/.ldflags; \ echo "-X ${PKG}/version.version=${VERSION#v} -X ${PKG}/version.revision=${REVISION} -X ${PKG}/version.mainpkg=${PKG}" | tee /tmp/.ldflags; \
echo -n "${VERSION}" | tee /tmp/.version; echo -n "${VERSION}" | tee /tmp/.version;
FROM base AS build FROM base AS build
@ -52,9 +52,9 @@ COPY --from=releaser /out /
FROM alpine:${ALPINE_VERSION} FROM alpine:${ALPINE_VERSION}
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml COPY cmd/registry/config-dev.yml /etc/distribution/config.yml
COPY --from=binary /registry /bin/registry COPY --from=binary /registry /bin/registry
VOLUME ["/var/lib/registry"] VOLUME ["/var/lib/registry"]
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["registry"] ENTRYPOINT ["registry"]
CMD ["serve", "/etc/docker/registry/config.yml"] CMD ["serve", "/etc/distribution/config.yml"]

View file

@ -37,7 +37,7 @@ WHALE = "+"
TESTFLAGS_RACE= TESTFLAGS_RACE=
GOFILES=$(shell find . -type f -name '*.go') GOFILES=$(shell find . -type f -name '*.go')
GO_TAGS=$(if $(BUILDTAGS),-tags "$(BUILDTAGS)",) GO_TAGS=$(if $(BUILDTAGS),-tags "$(BUILDTAGS)",)
GO_LDFLAGS=-ldflags '-extldflags "-Wl,-z,now" -s -w -X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revision=$(REVISION) -X $(PKG)/version.Package=$(PKG) $(EXTRA_LDFLAGS)' GO_LDFLAGS=-ldflags '-extldflags "-Wl,-z,now" -s -w -X $(PKG)/version.version=$(VERSION) -X $(PKG)/version.revision=$(REVISION) -X $(PKG)/version.mainpkg=$(PKG) $(EXTRA_LDFLAGS)'
BINARIES=$(addprefix bin/,$(COMMANDS)) BINARIES=$(addprefix bin/,$(COMMANDS))
@ -45,7 +45,7 @@ BINARIES=$(addprefix bin/,$(COMMANDS))
TESTFLAGS ?= -v $(TESTFLAGS_RACE) TESTFLAGS ?= -v $(TESTFLAGS_RACE)
TESTFLAGS_PARALLEL ?= 8 TESTFLAGS_PARALLEL ?= 8
.PHONY: all build binaries clean test test-race test-full integration test-coverage validate lint validate-git validate-vendor vendor mod-outdated image .PHONY: all build binaries clean test test-race test-full integration test-coverage validate lint validate-git validate-vendor vendor mod-outdated image validate-authors authors
.DEFAULT: all .DEFAULT: all
.PHONY: FORCE .PHONY: FORCE
@ -86,6 +86,9 @@ vendor: ## update vendor
mod-outdated: ## check outdated dependencies mod-outdated: ## check outdated dependencies
docker buildx bake $@ docker buildx bake $@
authors: ## generate authors
docker buildx bake $@
##@ Test ##@ Test
test: ## run tests, except integration test with test.short test: ## run tests, except integration test with test.short
@ -172,6 +175,9 @@ validate-git: ## validate git
validate-vendor: ## validate vendor validate-vendor: ## validate vendor
docker buildx bake $@ docker buildx bake $@
validate-authors: ## validate authors
docker buildx bake $@
.PHONY: help .PHONY: help
help: help:
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z0-9_\/%-]+:.*?##/ { printf " \033[36m%-27s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

View file

@ -2,7 +2,7 @@
<img style="align: center; padding-left: 10px; padding-right: 10px; padding-bottom: 10px;" width="238px" height="238px" src="./distribution-logo.svg" /> <img style="align: center; padding-left: 10px; padding-right: 10px; padding-bottom: 10px;" width="238px" height="238px" src="./distribution-logo.svg" />
</p> </p>
[![Build Status](https://github.com/distribution/distribution/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/distribution/distribution/actions?query=workflow%3ACI) [![Build Status](https://github.com/distribution/distribution/workflows/build/badge.svg?branch=main&event=push)](https://github.com/distribution/distribution/actions/workflows/build.yml?query=workflow%3Abuild)
[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/distribution) [![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/distribution/distribution)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
[![codecov](https://codecov.io/gh/distribution/distribution/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/distribution) [![codecov](https://codecov.io/gh/distribution/distribution/branch/main/graph/badge.svg)](https://codecov.io/gh/distribution/distribution)
@ -27,7 +27,7 @@ This repository contains the following components:
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **registry** | An implementation of the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). | | **registry** | An implementation of the [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec). |
| **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://pkg.go.dev/github.com/distribution/distribution) for details. **Note**: The interfaces for these libraries are **unstable**. | | **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://pkg.go.dev/github.com/distribution/distribution) for details. **Note**: The interfaces for these libraries are **unstable**. |
| **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/) related just to the registry. | | **documentation** | Full documentation is available at [https://distribution.github.io/distribution](https://distribution.github.io/distribution/).
### How does this integrate with Docker, containerd, and other OCI client? ### How does this integrate with Docker, containerd, and other OCI client?

View file

@ -140,12 +140,6 @@ type BlobDescriptorServiceFactory interface {
BlobAccessController(svc BlobDescriptorService) BlobDescriptorService BlobAccessController(svc BlobDescriptorService) BlobDescriptorService
} }
// ReadSeekCloser is the primary reader type for blob data, combining
// io.ReadSeeker with io.Closer.
//
// Deprecated: use [io.ReadSeekCloser].
type ReadSeekCloser = io.ReadSeekCloser
// BlobProvider describes operations for getting blob data. // BlobProvider describes operations for getting blob data.
type BlobProvider interface { type BlobProvider interface {
// Get returns the entire blob identified by digest along with the descriptor. // Get returns the entire blob identified by digest along with the descriptor.

View file

@ -12,6 +12,8 @@ storage:
maintenance: maintenance:
uploadpurging: uploadpurging:
enabled: false enabled: false
tag:
concurrencylimit: 8
http: http:
addr: :5000 addr: :5000
secret: asecretforlocaldevelopment secret: asecretforlocaldevelopment
@ -20,11 +22,10 @@ http:
headers: headers:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
redis: redis:
addr: localhost:6379 addrs: [localhost:6379]
pool: maxidleconns: 16
maxidle: 16 poolsize: 64
maxactive: 64 connmaxidletime: 300s
idletimeout: 300s
dialtimeout: 10ms dialtimeout: 10ms
readtimeout: 10ms readtimeout: 10ms
writetimeout: 10ms writetimeout: 10ms

View file

@ -14,6 +14,8 @@ storage:
maintenance: maintenance:
uploadpurging: uploadpurging:
enabled: false enabled: false
tag:
concurrencylimit: 8
http: http:
addr: :5000 addr: :5000
debug: debug:

View file

@ -7,6 +7,8 @@ storage:
blobdescriptor: inmemory blobdescriptor: inmemory
filesystem: filesystem:
rootdirectory: /var/lib/registry rootdirectory: /var/lib/registry
tag:
concurrencylimit: 8
http: http:
addr: :5000 addr: :5000
headers: headers:

View file

@ -15,6 +15,7 @@ import (
_ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/cloudfront" _ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/cloudfront"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/redirect" _ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/redirect"
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/rewrite"
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws" _ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
) )

View file

@ -8,6 +8,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"time" "time"
"github.com/redis/go-redis/v9"
) )
// Configuration is a versioned registry configuration, intended to be provided by a yaml file, and // Configuration is a versioned registry configuration, intended to be provided by a yaml file, and
@ -157,9 +159,15 @@ type Configuration struct {
// HTTP2 configuration options // HTTP2 configuration options
HTTP2 struct { HTTP2 struct {
// Specifies whether the registry should disallow clients attempting // Specifies whether the registry should disallow clients attempting
// to connect via http2. If set to true, only http/1.1 is supported. // to connect via HTTP/2. If set to true, only HTTP/1.1 is supported.
Disabled bool `yaml:"disabled,omitempty"` Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"http2,omitempty"` } `yaml:"http2,omitempty"`
H2C struct {
// Enables H2C (HTTP/2 Cleartext). Enable to support HTTP/2 without needing to configure TLS
// Useful when deploying the registry behind a load balancer (e.g. Cloud Run)
Enabled bool `yaml:"enabled,omitempty"`
} `yaml:"h2c,omitempty"`
} `yaml:"http,omitempty"` } `yaml:"http,omitempty"`
// Notifications specifies configuration about various endpoint to which // Notifications specifies configuration about various endpoint to which
@ -175,25 +183,7 @@ type Configuration struct {
Proxy Proxy `yaml:"proxy,omitempty"` Proxy Proxy `yaml:"proxy,omitempty"`
// Validation configures validation options for the registry. // Validation configures validation options for the registry.
Validation struct { Validation Validation `yaml:"validation,omitempty"`
// Enabled enables the other options in this section. This field is
// deprecated in favor of Disabled.
Enabled bool `yaml:"enabled,omitempty"`
// Disabled disables the other options in this section.
Disabled bool `yaml:"disabled,omitempty"`
// Manifests configures manifest validation.
Manifests struct {
// URLs configures validation for URLs in pushed manifests.
URLs struct {
// Allow specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must match.
Allow []string `yaml:"allow,omitempty"`
// Deny specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must not match.
Deny []string `yaml:"deny,omitempty"`
} `yaml:"urls,omitempty"`
} `yaml:"manifests,omitempty"`
} `yaml:"validation,omitempty"`
// Policy configures registry policy options. // Policy configures registry policy options.
Policy struct { Policy struct {
@ -271,44 +261,6 @@ type FileChecker struct {
Threshold int `yaml:"threshold,omitempty"` Threshold int `yaml:"threshold,omitempty"`
} }
// Redis configures the redis pool available to the registry webapp.
type Redis struct {
// Addr specifies the the redis instance available to the application.
Addr string `yaml:"addr,omitempty"`
// Usernames can be used as a finer-grained permission control since the introduction of the redis 6.0.
Username string `yaml:"username,omitempty"`
// Password string to use when making a connection.
Password string `yaml:"password,omitempty"`
// DB specifies the database to connect to on the redis instance.
DB int `yaml:"db,omitempty"`
// TLS configures settings for redis in-transit encryption
TLS struct {
Enabled bool `yaml:"enabled,omitempty"`
} `yaml:"tls,omitempty"`
DialTimeout time.Duration `yaml:"dialtimeout,omitempty"` // timeout for connect
ReadTimeout time.Duration `yaml:"readtimeout,omitempty"` // timeout for reads of data
WriteTimeout time.Duration `yaml:"writetimeout,omitempty"` // timeout for writes of data
// Pool configures the behavior of the redis connection pool.
Pool struct {
// MaxIdle sets the maximum number of idle connections.
MaxIdle int `yaml:"maxidle,omitempty"`
// MaxActive sets the maximum number of connections that should be
// opened before blocking a connection request.
MaxActive int `yaml:"maxactive,omitempty"`
// IdleTimeout sets the amount time to wait before closing
// inactive connections.
IdleTimeout time.Duration `yaml:"idletimeout,omitempty"`
} `yaml:"pool,omitempty"`
}
// HTTPChecker is a type of entry in the health section for checking HTTP URIs. // HTTPChecker is a type of entry in the health section for checking HTTP URIs.
type HTTPChecker struct { type HTTPChecker struct {
// Timeout is the duration to wait before timing out the HTTP request // Timeout is the duration to wait before timing out the HTTP request
@ -360,6 +312,13 @@ type Health struct {
} `yaml:"storagedriver,omitempty"` } `yaml:"storagedriver,omitempty"`
} }
type Platform struct {
// Architecture is the architecture for this platform
Architecture string `yaml:"architecture,omitempty"`
// OS is the operating system for this platform
OS string `yaml:"os,omitempty"`
}
// v0_1Configuration is a Version 0.1 Configuration struct // v0_1Configuration is a Version 0.1 Configuration struct
// This is currently aliased to Configuration, as it is the current version // This is currently aliased to Configuration, as it is the current version
type v0_1Configuration Configuration type v0_1Configuration Configuration
@ -435,6 +394,8 @@ func (storage Storage) Type() string {
// allow configuration of delete // allow configuration of delete
case "redirect": case "redirect":
// allow configuration of redirect // allow configuration of redirect
case "tag":
// allow configuration of tag
default: default:
storageType = append(storageType, k) storageType = append(storageType, k)
} }
@ -448,6 +409,19 @@ func (storage Storage) Type() string {
return "" return ""
} }
// TagParameters returns the Parameters map for a Storage tag configuration
func (storage Storage) TagParameters() Parameters {
return storage["tag"]
}
// setTagParameter changes the parameter at the provided key to the new value
func (storage Storage) setTagParameter(key string, value interface{}) {
if _, ok := storage["tag"]; !ok {
storage["tag"] = make(Parameters)
}
storage["tag"][key] = value
}
// Parameters returns the Parameters map for a Storage configuration // Parameters returns the Parameters map for a Storage configuration
func (storage Storage) Parameters() Parameters { func (storage Storage) Parameters() Parameters {
return storage[storage.Type()] return storage[storage.Type()]
@ -476,6 +450,8 @@ func (storage *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error {
// allow configuration of delete // allow configuration of delete
case "redirect": case "redirect":
// allow configuration of redirect // allow configuration of redirect
case "tag":
// allow configuration of tag
default: default:
types = append(types, k) types = append(types, k)
} }
@ -630,6 +606,62 @@ type Proxy struct {
TTL *time.Duration `yaml:"ttl,omitempty"` TTL *time.Duration `yaml:"ttl,omitempty"`
} }
type Validation struct {
// Enabled enables the other options in this section. This field is
// deprecated in favor of Disabled.
Enabled bool `yaml:"enabled,omitempty"`
// Disabled disables the other options in this section.
Disabled bool `yaml:"disabled,omitempty"`
// Manifests configures manifest validation.
Manifests ValidationManifests `yaml:"manifests,omitempty"`
}
type ValidationManifests struct {
// URLs configures validation for URLs in pushed manifests.
URLs struct {
// Allow specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must match.
Allow []string `yaml:"allow,omitempty"`
// Deny specifies regular expressions (https://godoc.org/regexp/syntax)
// that URLs in pushed manifests must not match.
Deny []string `yaml:"deny,omitempty"`
} `yaml:"urls,omitempty"`
// ImageIndexes configures validation of image indexes
Indexes ValidationIndexes `yaml:"indexes,omitempty"`
}
type ValidationIndexes struct {
// Platforms configures the validation applies to the platform images included in an image index
Platforms Platforms `yaml:"platforms"`
// PlatformList filters the set of platforms to validate for image existence.
PlatformList []Platform `yaml:"platformlist,omitempty"`
}
// Platforms configures the validation applies to the platform images included in an image index
// This can be all, none, or list
type Platforms string
// UnmarshalYAML implements the yaml.Umarshaler interface
// Unmarshals a string into a Platforms option, lowercasing the string and validating that it represents a
// valid option
func (platforms *Platforms) UnmarshalYAML(unmarshal func(interface{}) error) error {
var platformsString string
err := unmarshal(&platformsString)
if err != nil {
return err
}
platformsString = strings.ToLower(platformsString)
switch platformsString {
case "all", "none", "list":
default:
return fmt.Errorf("invalid platforms option %s Must be one of [all, none, list]", platformsString)
}
*platforms = Platforms(platformsString)
return nil
}
// Parse parses an input configuration yaml document into a Configuration struct // Parse parses an input configuration yaml document into a Configuration struct
// This should generally be capable of handling old configuration format versions // This should generally be capable of handling old configuration format versions
// //
@ -682,3 +714,172 @@ func Parse(rd io.Reader) (*Configuration, error) {
return config, nil return config, nil
} }
type RedisOptions = redis.UniversalOptions
type RedisTLSOptions struct {
Certificate string `yaml:"certificate,omitempty"`
Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"`
}
type Redis struct {
Options RedisOptions `yaml:",inline"`
TLS RedisTLSOptions `yaml:"tls,omitempty"`
}
func (c Redis) MarshalYAML() (interface{}, error) {
fields := make(map[string]interface{})
val := reflect.ValueOf(c.Options)
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldValue := val.Field(i)
// ignore funcs fields in redis.UniversalOptions
if fieldValue.Kind() == reflect.Func {
continue
}
fields[strings.ToLower(field.Name)] = fieldValue.Interface()
}
// Add TLS fields if they're not empty
if c.TLS.Certificate != "" || c.TLS.Key != "" || len(c.TLS.ClientCAs) > 0 {
fields["tls"] = c.TLS
}
return fields, nil
}
func (c *Redis) UnmarshalYAML(unmarshal func(interface{}) error) error {
var fields map[string]interface{}
err := unmarshal(&fields)
if err != nil {
return err
}
val := reflect.ValueOf(&c.Options).Elem()
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fieldName := strings.ToLower(field.Name)
if value, ok := fields[fieldName]; ok {
fieldValue := val.Field(i)
if fieldValue.CanSet() {
switch field.Type {
case reflect.TypeOf(time.Duration(0)):
durationStr, ok := value.(string)
if !ok {
return fmt.Errorf("invalid duration value for field: %s", fieldName)
}
duration, err := time.ParseDuration(durationStr)
if err != nil {
return fmt.Errorf("failed to parse duration for field: %s, error: %v", fieldName, err)
}
fieldValue.Set(reflect.ValueOf(duration))
default:
if err := setFieldValue(fieldValue, value); err != nil {
return fmt.Errorf("failed to set value for field: %s, error: %v", fieldName, err)
}
}
}
}
}
// Handle TLS fields
if tlsData, ok := fields["tls"]; ok {
tlsMap, ok := tlsData.(map[interface{}]interface{})
if !ok {
return fmt.Errorf("invalid TLS data structure")
}
if cert, ok := tlsMap["certificate"]; ok {
var isString bool
c.TLS.Certificate, isString = cert.(string)
if !isString {
return fmt.Errorf("Redis TLS certificate must be a string")
}
}
if key, ok := tlsMap["key"]; ok {
var isString bool
c.TLS.Key, isString = key.(string)
if !isString {
return fmt.Errorf("Redis TLS (private) key must be a string")
}
}
if cas, ok := tlsMap["clientcas"]; ok {
caList, ok := cas.([]interface{})
if !ok {
return fmt.Errorf("invalid clientcas data structure")
}
for _, ca := range caList {
if caStr, ok := ca.(string); ok {
c.TLS.ClientCAs = append(c.TLS.ClientCAs, caStr)
}
}
}
}
return nil
}
func setFieldValue(field reflect.Value, value interface{}) error {
if value == nil {
return nil
}
switch field.Kind() {
case reflect.String:
stringValue, ok := value.(string)
if !ok {
return fmt.Errorf("failed to convert value to string")
}
field.SetString(stringValue)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
intValue, ok := value.(int)
if !ok {
return fmt.Errorf("failed to convert value to integer")
}
field.SetInt(int64(intValue))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
uintValue, ok := value.(uint)
if !ok {
return fmt.Errorf("failed to convert value to unsigned integer")
}
field.SetUint(uint64(uintValue))
case reflect.Float32, reflect.Float64:
floatValue, ok := value.(float64)
if !ok {
return fmt.Errorf("failed to convert value to float")
}
field.SetFloat(floatValue)
case reflect.Bool:
boolValue, ok := value.(bool)
if !ok {
return fmt.Errorf("failed to convert value to boolean")
}
field.SetBool(boolValue)
case reflect.Slice:
slice := reflect.MakeSlice(field.Type(), 0, 0)
valueSlice, ok := value.([]interface{})
if !ok {
return fmt.Errorf("failed to convert value to slice")
}
for _, item := range valueSlice {
sliceValue := reflect.New(field.Type().Elem()).Elem()
if err := setFieldValue(sliceValue, item); err != nil {
return err
}
slice = reflect.Append(slice, sliceValue)
}
field.Set(slice)
default:
return fmt.Errorf("unsupported field type: %v", field.Type())
}
return nil
}

View file

@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -39,6 +40,9 @@ var configStruct = Configuration{
"url1": "https://foo.example.com", "url1": "https://foo.example.com",
"path1": "/some-path", "path1": "/some-path",
}, },
"tag": Parameters{
"concurrencylimit": 10,
},
}, },
Auth: Auth{ Auth: Auth{
"silly": Parameters{ "silly": Parameters{
@ -97,6 +101,9 @@ var configStruct = Configuration{
HTTP2 struct { HTTP2 struct {
Disabled bool `yaml:"disabled,omitempty"` Disabled bool `yaml:"disabled,omitempty"`
} `yaml:"http2,omitempty"` } `yaml:"http2,omitempty"`
H2C struct {
Enabled bool `yaml:"enabled,omitempty"`
} `yaml:"h2c,omitempty"`
}{ }{
TLS: struct { TLS: struct {
Certificate string `yaml:"certificate,omitempty"` Certificate string `yaml:"certificate,omitempty"`
@ -121,25 +128,38 @@ var configStruct = Configuration{
}{ }{
Disabled: false, Disabled: false,
}, },
H2C: struct {
Enabled bool `yaml:"enabled,omitempty"`
}{
Enabled: true,
},
}, },
Redis: Redis{ Redis: Redis{
Addr: "localhost:6379", Options: redis.UniversalOptions{
Addrs: []string{"localhost:6379"},
Username: "alice", Username: "alice",
Password: "123456", Password: "123456",
DB: 1, DB: 1,
Pool: struct { MaxIdleConns: 16,
MaxIdle int `yaml:"maxidle,omitempty"` PoolSize: 64,
MaxActive int `yaml:"maxactive,omitempty"` ConnMaxIdleTime: time.Second * 300,
IdleTimeout time.Duration `yaml:"idletimeout,omitempty"`
}{
MaxIdle: 16,
MaxActive: 64,
IdleTimeout: time.Second * 300,
},
DialTimeout: time.Millisecond * 10, DialTimeout: time.Millisecond * 10,
ReadTimeout: time.Millisecond * 10, ReadTimeout: time.Millisecond * 10,
WriteTimeout: time.Millisecond * 10, WriteTimeout: time.Millisecond * 10,
}, },
TLS: RedisTLSOptions{
Certificate: "/foo/cert.crt",
Key: "/foo/key.pem",
ClientCAs: []string{"/path/to/ca.pem"},
},
},
Validation: Validation{
Manifests: ValidationManifests{
Indexes: ValidationIndexes{
Platforms: "none",
},
},
},
} }
// configYamlV0_1 is a Version 0.1 yaml document representing configStruct // configYamlV0_1 is a Version 0.1 yaml document representing configStruct
@ -159,6 +179,8 @@ storage:
int1: 42 int1: 42
url1: "https://foo.example.com" url1: "https://foo.example.com"
path1: "/some-path" path1: "/some-path"
tag:
concurrencylimit: 10
auth: auth:
silly: silly:
realm: silly realm: silly
@ -177,22 +199,31 @@ notifications:
actions: actions:
- pull - pull
http: http:
tls:
clientcas: clientcas:
- /path/to/ca.pem - /path/to/ca.pem
headers: headers:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
redis: redis:
addr: localhost:6379 tls:
certificate: /foo/cert.crt
key: /foo/key.pem
clientcas:
- /path/to/ca.pem
addrs: [localhost:6379]
username: alice username: alice
password: 123456 password: "123456"
db: 1 db: 1
pool: maxidleconns: 16
maxidle: 16 poolsize: 64
maxactive: 64 connmaxidletime: 300s
idletimeout: 300s
dialtimeout: 10ms dialtimeout: 10ms
readtimeout: 10ms readtimeout: 10ms
writetimeout: 10ms writetimeout: 10ms
validation:
manifests:
indexes:
platforms: none
` `
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
@ -222,6 +253,10 @@ notifications:
http: http:
headers: headers:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
validation:
manifests:
indexes:
platforms: none
` `
type ConfigSuite struct { type ConfigSuite struct {
@ -261,6 +296,7 @@ func (suite *ConfigSuite) TestParseSimple() {
func (suite *ConfigSuite) TestParseInmemory() { func (suite *ConfigSuite) TestParseInmemory() {
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
suite.expectedConfig.Log.Fields = nil suite.expectedConfig.Log.Fields = nil
suite.expectedConfig.HTTP.TLS.ClientCAs = nil
suite.expectedConfig.Redis = Redis{} suite.expectedConfig.Redis = Redis{}
config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1))) config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1)))
@ -281,7 +317,9 @@ func (suite *ConfigSuite) TestParseIncomplete() {
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}} suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.Notifications = Notifications{}
suite.expectedConfig.HTTP.Headers = nil suite.expectedConfig.HTTP.Headers = nil
suite.expectedConfig.HTTP.TLS.ClientCAs = nil
suite.expectedConfig.Redis = Redis{} suite.expectedConfig.Redis = Redis{}
suite.expectedConfig.Validation.Manifests.Indexes.Platforms = ""
// Note: this also tests that REGISTRY_STORAGE and // Note: this also tests that REGISTRY_STORAGE and
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
@ -534,6 +572,9 @@ func copyConfig(config Configuration) *Configuration {
for k, v := range config.Storage.Parameters() { for k, v := range config.Storage.Parameters() {
configCopy.Storage.setParameter(k, v) configCopy.Storage.setParameter(k, v)
} }
for k, v := range config.Storage.TagParameters() {
configCopy.Storage.setTagParameter(k, v)
}
configCopy.Auth = Auth{config.Auth.Type(): Parameters{}} configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
for k, v := range config.Auth.Parameters() { for k, v := range config.Auth.Parameters() {
@ -547,8 +588,20 @@ func copyConfig(config Configuration) *Configuration {
for k, v := range config.HTTP.Headers { for k, v := range config.HTTP.Headers {
configCopy.HTTP.Headers[k] = v configCopy.HTTP.Headers[k] = v
} }
configCopy.HTTP.TLS.ClientCAs = make([]string, 0, len(config.HTTP.TLS.ClientCAs))
configCopy.HTTP.TLS.ClientCAs = append(configCopy.HTTP.TLS.ClientCAs, config.HTTP.TLS.ClientCAs...)
configCopy.Redis = config.Redis configCopy.Redis = config.Redis
configCopy.Redis.TLS.Certificate = config.Redis.TLS.Certificate
configCopy.Redis.TLS.Key = config.Redis.TLS.Key
configCopy.Redis.TLS.ClientCAs = make([]string, 0, len(config.Redis.TLS.ClientCAs))
configCopy.Redis.TLS.ClientCAs = append(configCopy.Redis.TLS.ClientCAs, config.Redis.TLS.ClientCAs...)
configCopy.Validation = Validation{
Enabled: config.Validation.Enabled,
Disabled: config.Validation.Disabled,
Manifests: config.Validation.Manifests,
}
return configCopy return configCopy
} }

View file

@ -39,11 +39,7 @@ target "update-vendor" {
target "mod-outdated" { target "mod-outdated" {
dockerfile = "./dockerfiles/vendor.Dockerfile" dockerfile = "./dockerfiles/vendor.Dockerfile"
target = "outdated" target = "outdated"
args = { no-cache-filter = ["outdated"]
// used to invalidate cache for outdated run stage
// can be dropped when https://github.com/moby/buildkit/issues/1213 fixed
_RANDOM = uuidv4()
}
output = ["type=cacheonly"] output = ["type=cacheonly"]
} }
@ -95,15 +91,8 @@ target "image-all" {
] ]
} }
variable "DOCS_BASEURL" {
default = null
}
target "_common_docs" { target "_common_docs" {
dockerfile = "./dockerfiles/docs.Dockerfile" dockerfile = "./dockerfiles/docs.Dockerfile"
args = {
DOCS_BASEURL = DOCS_BASEURL
}
} }
target "docs-export" { target "docs-export" {
@ -124,3 +113,15 @@ target "docs-test" {
target = "test" target = "test"
output = ["type=cacheonly"] output = ["type=cacheonly"]
} }
target "authors" {
dockerfile = "./dockerfiles/authors.Dockerfile"
target = "update"
output = ["."]
}
target "validate-authors" {
dockerfile = "./dockerfiles/authors.Dockerfile"
target = "validate"
output = ["type=cacheonly"]
}

View file

@ -0,0 +1,34 @@
# syntax=docker/dockerfile:1
ARG ALPINE_VERSION=3.20
FROM alpine:${ALPINE_VERSION} AS gen
RUN apk add --no-cache git
WORKDIR /src
RUN --mount=type=bind,target=. <<EOT
set -e
mkdir /out
# see also ".mailmap" for how email addresses and names are deduplicated
{
echo "# This file lists all individuals having contributed content to the repository."
echo "# For how it is generated, see dockerfiles/authors.Dockerfile."
echo
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf
} > /out/AUTHORS
cat /out/AUTHORS
EOT
FROM scratch AS update
COPY --from=gen /out /
FROM gen AS validate
RUN --mount=type=bind,target=.,rw <<EOT
set -e
git add -A
cp -rf /out/* .
if [ -n "$(git status --porcelain -- AUTHORS)" ]; then
echo >&2 'ERROR: Authors result differs. Please update with "make authors"'
git status --porcelain -- AUTHORS
exit 1
fi
EOT

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.21.5 ARG GO_VERSION=1.22.4
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
RUN apk add --no-cache git RUN apk add --no-cache git
@ -16,9 +16,8 @@ COPY --from=hugo $GOPATH/bin/hugo /bin/hugo
WORKDIR /src WORKDIR /src
FROM build-base AS build FROM build-base AS build
ARG DOCS_BASEURL=/
RUN --mount=type=bind,rw,source=docs,target=. \ RUN --mount=type=bind,rw,source=docs,target=. \
hugo --gc --minify --destination /out -b $DOCS_BASEURL hugo --gc --minify --destination /out
FROM build-base AS server FROM build-base AS server
COPY docs . COPY docs .
@ -29,8 +28,12 @@ FROM scratch AS out
COPY --from=build /out / COPY --from=build /out /
FROM wjdp/htmltest:v0.17.0 AS test FROM wjdp/htmltest:v0.17.0 AS test
# Copy the site to a public/distribution subdirectory
# This is a workaround for a limitation in htmltest, see:
# https://github.com/wjdp/htmltest/issues/45
WORKDIR /test/public/distribution
COPY --from=build /out .
WORKDIR /test WORKDIR /test
COPY --from=build /out ./public
ADD docs/.htmltest.yml .htmltest.yml ADD docs/.htmltest.yml .htmltest.yml
RUN --mount=type=cache,target=tmp/.htmltest \ RUN --mount=type=cache,target=tmp/.htmltest \
htmltest htmltest

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.20.12 ARG GO_VERSION=1.22.4
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
FROM alpine:${ALPINE_VERSION} AS base FROM alpine:${ALPINE_VERSION} AS base
RUN apk add --no-cache git gpg RUN apk add --no-cache git gpg

View file

@ -1,8 +1,8 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.20.12 ARG GO_VERSION=1.22.4
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
ARG GOLANGCI_LINT_VERSION=v1.55.2 ARG GOLANGCI_LINT_VERSION=v1.59.1
ARG BUILDTAGS="" ARG BUILDTAGS=""
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG GO_VERSION=1.20.12 ARG GO_VERSION=1.22.4
ARG ALPINE_VERSION=3.18 ARG ALPINE_VERSION=3.20
ARG MODOUTDATED_VERSION=v0.8.0 ARG MODOUTDATED_VERSION=v0.8.0
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
@ -40,7 +40,6 @@ EOT
FROM psampaz/go-mod-outdated:${MODOUTDATED_VERSION} AS go-mod-outdated FROM psampaz/go-mod-outdated:${MODOUTDATED_VERSION} AS go-mod-outdated
FROM base AS outdated FROM base AS outdated
ARG _RANDOM
RUN --mount=target=.,ro \ RUN --mount=target=.,ro \
--mount=target=/go/pkg/mod,type=cache \ --mount=target=/go/pkg/mod,type=cache \
--mount=from=go-mod-outdated,source=/home/go-mod-outdated,target=/usr/bin/go-mod-outdated \ --mount=from=go-mod-outdated,source=/home/go-mod-outdated,target=/usr/bin/go-mod-outdated \

View file

@ -50,7 +50,7 @@ specify it in the `docker run` command:
```bash ```bash
$ docker run -d -p 5000:5000 --restart=always --name registry \ $ docker run -d -p 5000:5000 --restart=always --name registry \
-v `pwd`/config.yml:/etc/docker/registry/config.yml \ -v `pwd`/config.yml:/etc/distribution/config.yml \
registry:2 registry:2
``` ```
@ -141,6 +141,8 @@ storage:
usedualstack: false usedualstack: false
loglevel: debug loglevel: debug
inmemory: # This driver takes no parameters inmemory: # This driver takes no parameters
tag:
concurrencylimit: 8
delete: delete:
enabled: false enabled: false
redirect: redirect:
@ -166,6 +168,10 @@ auth:
service: token-service service: token-service
issuer: registry-token-issuer issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle rootcertbundle: /root/certs/bundle
jwks: /path/to/jwks
signingalgorithms:
- EdDSA
- HS256
htpasswd: htpasswd:
realm: basic-realm realm: basic-realm
path: /path/to/htpasswd path: /path/to/htpasswd
@ -220,6 +226,8 @@ http:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
http2: http2:
disabled: false disabled: false
h2c:
enabled: false
notifications: notifications:
events: events:
includereferences: true includereferences: true
@ -239,16 +247,20 @@ notifications:
actions: actions:
- pull - pull
redis: redis:
addr: localhost:6379 tls:
certificate: /path/to/cert.crt
key: /path/to/key.pem
clientcas:
- /path/to/ca.pem
addrs: [localhost:6379]
password: asecret password: asecret
db: 0 db: 0
dialtimeout: 10ms dialtimeout: 10ms
readtimeout: 10ms readtimeout: 10ms
writetimeout: 10ms writetimeout: 10ms
pool: maxidleconns: 16
maxidle: 16 poolsize: 64
maxactive: 64 connmaxidletime: 300s
idletimeout: 300s
tls: tls:
enabled: false enabled: false
health: health:
@ -284,6 +296,11 @@ validation:
- ^https?://([^/]+\.)*example\.com/ - ^https?://([^/]+\.)*example\.com/
deny: deny:
- ^https?://www\.example\.com/ - ^https?://www\.example\.com/
indexes:
platforms: List
platformlist:
- architecture: amd64
os: linux
``` ```
In some instances a configuration option is **optional** but it contains child In some instances a configuration option is **optional** but it contains child
@ -435,16 +452,16 @@ use. You must configure exactly one backend. If you configure more, the registry
returns an error. You can choose any of these backend storage drivers: returns an error. You can choose any of these backend storage drivers:
| Storage driver | Description | | Storage driver | Description |
|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `filesystem` | Uses the local disk to store registry files. It is ideal for development and may be appropriate for some small-scale production applications. See the [driver's reference documentation](/storage-drivers/filesystem). | | `filesystem` | Uses the local disk to store registry files. It is ideal for development and may be appropriate for some small-scale production applications. See the [driver's reference documentation](../storage-drivers/filesystem.md). |
| `azure` | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](/storage-drivers/azure). | | `azure` | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](../storage-drivers/azure.md). |
| `gcs` | Uses Google Cloud Storage. See the [driver's reference documentation](/storage-drivers/gcs). | | `gcs` | Uses Google Cloud Storage. See the [driver's reference documentation](../storage-drivers/gcs.md). |
| `s3` | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](/storage-drivers/s3). | | `s3` | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](../storage-drivers/s3.md). |
For testing only, you can use the [`inmemory` storage For testing only, you can use the [`inmemory` storage
driver](/storage-drivers/inmemory). driver](../storage-drivers/inmemory.md).
If you would like to run a registry from volatile memory, use the If you would like to run a registry from volatile memory, use the
[`filesystem` driver](/storage-drivers/filesystem) [`filesystem` driver](../storage-drivers/filesystem.md)
on a ramdisk. on a ramdisk.
If you are deploying a registry on Windows, a Windows volume mounted from the If you are deploying a registry on Windows, a Windows volume mounted from the
@ -519,6 +536,26 @@ parameter sets a limit on the number of descriptors to store in the cache.
The default value is 10000. If this parameter is set to 0, the cache is allowed The default value is 10000. If this parameter is set to 0, the cache is allowed
to grow with no size limit. to grow with no size limit.
### `tag`
The `tag` subsection provides configuration to set concurrency limit for tag lookup.
When user calls into the registry to delete the manifest, which in turn then does a
lookup for all tags that reference the deleted manifest. To find the tag references,
the registry will iterate every tag in the repository and read it's link file to check
if it matches the deleted manifest (i.e. to see if uses the same sha256 digest).
So, the more tags in repository, the worse the performance will be (as there will
be more S3 API calls occurring for the tag directory lookups and tag file reads if
using S3 storage driver).
Therefore, add a single flag `concurrencylimit` to set concurrency limit to optimize tag
lookup performance under the `tag` section. When a value is not provided or equal to 0,
`GOMAXPROCS` will be used.
```yaml
tag:
concurrencylimit: 8
```
### `redirect` ### `redirect`
The `redirect` subsection provides configuration for managing redirects from The `redirect` subsection provides configuration for managing redirects from
@ -548,6 +585,11 @@ auth:
service: token-service service: token-service
issuer: registry-token-issuer issuer: registry-token-issuer
rootcertbundle: /root/certs/bundle rootcertbundle: /root/certs/bundle
jwks: /path/to/jwks
signingalgorithms:
- EdDSA
- HS256
- ES512
htpasswd: htpasswd:
realm: basic-realm realm: basic-realm
path: /path/to/htpasswd path: /path/to/htpasswd
@ -584,16 +626,48 @@ the registry. It is an established authentication paradigm with a high degree of
security. security.
| Parameter | Required | Description | | Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------| |----------------------|----------|-------------------------------------------------------|
| `realm` | yes | The realm in which the registry server authenticates. | | `realm` | yes | The realm in which the registry server authenticates. |
| `service` | yes | The service being authenticated. | | `service` | yes | The service being authenticated. |
| `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. | | `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. |
| `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. | | `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. |
| `autoredirect` | no | When set to `true`, `realm` will automatically be set using the Host header of the request as the domain and a path of `/auth/token/`| | `autoredirect` | no | When set to `true`, `realm` will be set to the Host header of the request as the domain and a path of `/auth/token/`(or specified by `autoredirectpath`), the `realm` URL Scheme will use `X-Forwarded-Proto` header if set, otherwise it will be set to `https`. |
| `autoredirectpath` | no | The path to redirect to if `autoredirect` is set to `true`, default: `/auth/token/`. |
| `signingalgorithms` | no | A list of token signing algorithms to use for verifying token signatures. If left empty the default list of signing algorithms is used. Please see below for allowed values and default. |
| `jwks` | no | The absolute path to the JSON Web Key Set (JWKS) file. The JWKS file contains the trusted keys used to verify the signature of authentication tokens. |
Available `signingalgorithms`:
- EdDSA
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256
- ES384
- ES512
- PS256
- PS384
- PS512
Default `signingalgorithms`:
- EdDSA
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256
- ES384
- ES512
- PS256
- PS384
- PS512
For more information about Token based authentication configuration, see the For more information about Token based authentication configuration, see the
[specification](/spec/auth/token). [specification](../spec/auth/token.md).
### `htpasswd` ### `htpasswd`
@ -724,6 +798,8 @@ http:
X-Content-Type-Options: [nosniff] X-Content-Type-Options: [nosniff]
http2: http2:
disabled: false disabled: false
h2c:
enabled: false
``` ```
The `http` option details the configuration for the HTTP server that hosts the The `http` option details the configuration for the HTTP server that hosts the
@ -870,13 +946,24 @@ registry. This header is included in the example configuration file.
### `http2` ### `http2`
The `http2` structure within `http` is **optional**. Use this to control http2 The `http2` structure within `http` is **optional**. Use this to control HTTP/2 over TLS
settings for the registry. settings for the registry.
If `tls` is not configured this option is ignored. To enable HTTP/2 over non TLS connections use `h2c` instead.
| Parameter | Required | Description | | Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------| |-----------|----------|-------------------------------------------------------|
| `disabled` | no | If `true`, then `http2` support is disabled. | | `disabled` | no | If `true`, then `http2` support is disabled. |
### `h2c`
The `h2c` structure within `http` is **optional**. Use this to control H2C (HTTP/2 Cleartext)
settings for the registry.
Useful when deploying the registry behind a load balancer (e.g. Google Cloud Run)
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `enabled` | no | If `true`, then `h2c` support is enabled. |
## `notifications` ## `notifications`
```yaml ```yaml
@ -937,72 +1024,46 @@ The `events` structure configures the information provided in event notification
## `redis` ## `redis`
Declare parameters for constructing the `redis` connections. Registry instances
may use the Redis instance for several applications. Currently, it caches
information about immutable blobs. Most of the `redis` options control
how the registry connects to the `redis` instance.
You should configure Redis with the **allkeys-lru** eviction policy, because the
registry does not set an expiration value on keys.
Under the hood distribution uses [`go-redis`](https://github.com/redis/go-redis) Go module for
Redis connectivity and its [`UniversalOptions`](https://pkg.go.dev/github.com/redis/go-redis/v9#UniversalOptions)
struct.
You can optionally specify TLS configuration on top of the `UniversalOptions` settings.
Use these settings to configure Redis TLS:
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `certificate` | yes | Absolute path to the x509 certificate file. |
| `key` | yes | Absolute path to the x509 private key file. |
| `clientcas` | no | An array of absolute paths to x509 CA files. |
```yaml ```yaml
redis: redis:
addr: localhost:6379 tls:
certificate: /path/to/cert.crt
key: /path/to/key.pem
clientcas:
- /path/to/ca.pem
addrs: [localhost:6379]
password: asecret password: asecret
db: 0 db: 0
dialtimeout: 10ms dialtimeout: 10ms
readtimeout: 10ms readtimeout: 10ms
writetimeout: 10ms writetimeout: 10ms
pool: maxidleconns: 16
maxidle: 16 poolsize: 64
maxactive: 64 connmaxidletime: 300s
idletimeout: 300s
tls:
enabled: false
``` ```
Declare parameters for constructing the `redis` connections. Registry instances
may use the Redis instance for several applications. Currently, it caches
information about immutable blobs. Most of the `redis` options control
how the registry connects to the `redis` instance. You can control the pool's
behavior with the [pool](#pool) subsection. Additionally, you can control
TLS connection settings with the [tls](#tls) subsection (in-transit encryption).
You should configure Redis with the **allkeys-lru** eviction policy, because the
registry does not set an expiration value on keys.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `addr` | yes | The address (host and port) of the Redis instance. |
| `password`| no | A password used to authenticate to the Redis instance.|
| `db` | no | The name of the database to use for each connection. |
| `dialtimeout` | no | The timeout for connecting to the Redis instance. |
| `readtimeout` | no | The timeout for reading from the Redis instance. |
| `writetimeout` | no | The timeout for writing to the Redis instance. |
### `pool`
```yaml
pool:
maxidle: 16
maxactive: 64
idletimeout: 300s
```
Use these settings to configure the behavior of the Redis connection pool.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------------------------|
| `maxidle` | no | The maximum number of idle connections in the pool. |
| `maxactive`| no | The maximum number of connections which can be open before blocking a connection request. |
| `idletimeout`| no | How long to wait before closing inactive connections. |
### `tls`
```yaml
tls:
enabled: false
```
Use these settings to configure Redis TLS.
| Parameter | Required | Description |
|-----------|----------|-------------------------------------- |
| `enabled` | no | Whether or not to use TLS in-transit. |
## `health` ## `health`
```yaml ```yaml
@ -1100,7 +1161,7 @@ proxy:
The `proxy` structure allows a registry to be configured as a pull-through cache The `proxy` structure allows a registry to be configured as a pull-through cache
to Docker Hub. See to Docker Hub. See
[mirror](/recipes/mirror) [mirror](../recipes/mirror.md)
for more information. Pushing to a registry configured as a pull-through cache for more information. Pushing to a registry configured as a pull-through cache
is unsupported. is unsupported.
@ -1122,14 +1183,14 @@ username (such as `batman`) and the password for that username.
```yaml ```yaml
validation: validation:
manifests: disabled: false
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/
``` ```
Use these settings to configure what validation the registry performs on content.
Validation is performed when content is uploaded to the registry. Changing these
settings will not validate content that has already been accepting into the registry.
### `disabled` ### `disabled`
The `disabled` flag disables the other options in the `validation` The `disabled` flag disables the other options in the `validation`
@ -1142,6 +1203,16 @@ Use the `manifests` subsection to configure validation of manifests. If
#### `urls` #### `urls`
```yaml
validation:
manifests:
urls:
allow:
- ^https?://([^/]+\.)*example\.com/
deny:
- ^https?://www\.example\.com/
```
The `allow` and `deny` options are each a list of The `allow` and `deny` options are each a list of
[regular expressions](https://pkg.go.dev/regexp/syntax) that restrict the URLs in [regular expressions](https://pkg.go.dev/regexp/syntax) that restrict the URLs in
pushed manifests. pushed manifests.
@ -1155,6 +1226,54 @@ one of the `allow` regular expressions **and** one of the following holds:
2. `deny` is set but no URLs within the manifest match any of the `deny` regular 2. `deny` is set but no URLs within the manifest match any of the `deny` regular
expressions. expressions.
#### `indexes`
By default the registry will validate that all platform images exist when an image
index is uploaded to the registry. Disabling this validatation is experimental
because other tooling that uses the registry may expect the image index to be complete.
validation:
manifests:
indexes:
platforms: [all|none|list]
platformlist:
- os: linux
architecture: amd64
Use these settings to configure what validation the registry performs on image
index manifests uploaded to the registry.
##### `platforms`
Set `platformexist` to `all` (the default) to validate all platform images exist.
The registry will validate that the images referenced by the index exist in the
registry before accepting the image index.
Set `platforms` to `none` to disable all validation that images exist when an
image index manifest is uploaded. This allows image lists to be uploaded to the
registry without their associated images. This setting is experimental because
other tooling that uses the registry may expect the image index to be complete.
Set `platforms` to `list` to selectively validate the existence of platforms
within image index manifests. This setting is experimental because other tooling
that uses the registry may expect the image index to be complete.
##### `platformlist`
When `platforms` is set to `list`, set `platformlist` to an array of
platforms to validate. If a platform is included in this the array and in the images
contained within an index, the registry will validate that the platform specific image
exists in the registry before accepting the index. The registry will not validate the
existence of platform specific images in the index that do not appear in the
`platformlist` array.
This parameter does not validate that the configured platforms are included in every
index. If an image index does not include one of the platform specific images configured
in the `platformlist` array, it may still be accepted by the registry.
Each platform is a map with two keys, `os` and `architecture`, as defined in the
[OCI Image Index specification](https://github.com/opencontainers/image-spec/blob/main/image-index.md#image-index-property-descriptions).
## Example: Development configuration ## Example: Development configuration
You can use this simple example for local development: You can use this simple example for local development:

View file

@ -9,7 +9,7 @@ A registry is an instance of the `registry` image, and runs within Docker.
This topic provides basic information about deploying and configuring a This topic provides basic information about deploying and configuring a
registry. For an exhaustive list of configuration options, see the registry. For an exhaustive list of configuration options, see the
[configuration reference](../configuration). [configuration reference](configuration.md).
If you have an air-gapped datacenter, see If you have an air-gapped datacenter, see
[Considerations for air-gapped registries](#considerations-for-air-gapped-registries). [Considerations for air-gapped registries](#considerations-for-air-gapped-registries).
@ -27,7 +27,7 @@ The registry is now ready to use.
> **Warning**: These first few examples show registry configurations that are > **Warning**: These first few examples show registry configurations that are
> only appropriate for testing. A production-ready registry must be protected by > only appropriate for testing. A production-ready registry must be protected by
> TLS and should ideally use an access-control mechanism. Keep reading and then > TLS and should ideally use an access-control mechanism. Keep reading and then
> continue to the [configuration guide](../configuration) to deploy a > continue to the [configuration guide](configuration.md) to deploy a
> production-ready registry. > production-ready registry.
## Copy an image from Docker Hub to your registry ## Copy an image from Docker Hub to your registry
@ -94,7 +94,7 @@ To configure the container, you can pass additional or modified options to the
`docker run` command. `docker run` command.
The following sections provide basic guidelines for configuring your registry. The following sections provide basic guidelines for configuring your registry.
For more details, see the [registry configuration reference](../configuration). For more details, see the [registry configuration reference](configuration.md).
### Start the registry automatically ### Start the registry automatically
@ -166,8 +166,8 @@ $ docker run -d \
By default, the registry stores its data on the local filesystem, whether you By default, the registry stores its data on the local filesystem, whether you
use a bind mount or a volume. You can store the registry data in an Amazon S3 use a bind mount or a volume. You can store the registry data in an Amazon S3
bucket, Google Cloud Platform, or on another storage back-end by using bucket, Google Cloud Platform, or on another storage back-end by using
[storage drivers](/storage-drivers). For more information, see [storage drivers](../storage-drivers/_index.md). For more information, see
[storage configuration options](../configuration#storage). [storage configuration options](configuration.md#storage).
## Run an externally-accessible registry ## Run an externally-accessible registry
@ -252,13 +252,13 @@ The registry supports using Let's Encrypt to automatically obtain a
browser-trusted certificate. For more information on Let's Encrypt, see browser-trusted certificate. For more information on Let's Encrypt, see
[https://letsencrypt.org/how-it-works/](https://letsencrypt.org/how-it-works/) [https://letsencrypt.org/how-it-works/](https://letsencrypt.org/how-it-works/)
and the relevant section of the and the relevant section of the
[registry configuration](../configuration#letsencrypt). [registry configuration](configuration.md#letsencrypt).
### Use an insecure registry (testing only) ### Use an insecure registry (testing only)
It is possible to use a self-signed certificate, or to use our registry It is possible to use a self-signed certificate, or to use our registry
insecurely. Unless you have set up verification for your self-signed insecurely. Unless you have set up verification for your self-signed
certificate, this is for testing only. See [run an insecure registry](../insecure). certificate, this is for testing only. See [run an insecure registry](insecure.md).
## Run the registry as a service ## Run the registry as a service
@ -462,20 +462,20 @@ using htpasswd, all authentication attempts will fail.
{{< hint type=note title="X509 errors" >}} {{< hint type=note title="X509 errors" >}}
X509 errors usually indicate that you are attempting to use X509 errors usually indicate that you are attempting to use
a self-signed certificate without configuring the Docker daemon correctly. a self-signed certificate without configuring the Docker daemon correctly.
See [run an insecure registry](../insecure). See [run an insecure registry](insecure.md).
{{< /hint >}} {{< /hint >}}
### More advanced authentication ### More advanced authentication
You may want to leverage more advanced basic auth implementations by using a You may want to leverage more advanced basic auth implementations by using a
proxy in front of the registry. See the [recipes list](/recipes/). proxy in front of the registry. See the [recipes list](../recipes/_index.md).
The registry also supports delegated authentication which redirects users to a The registry also supports delegated authentication which redirects users to a
specific trusted token server. This approach is more complicated to set up, and specific trusted token server. This approach is more complicated to set up, and
only makes sense if you need to fully configure ACLs and need more control over only makes sense if you need to fully configure ACLs and need more control over
the registry's integration into your global authorization and authentication the registry's integration into your global authorization and authentication
systems. Refer to the following [background information](/spec/auth/token) and systems. Refer to the following [background information](../spec/auth/token.md) and
[configuration information here](../configuration#auth). [configuration information here](configuration.md#auth).
This approach requires you to implement your own authentication system or This approach requires you to implement your own authentication system or
leverage a third-party implementation. leverage a third-party implementation.
@ -572,9 +572,9 @@ artifacts.
More specific and advanced information is available in the following sections: More specific and advanced information is available in the following sections:
- [Configuration reference](../configuration) - [Configuration reference](configuration.md)
- [Working with notifications](../notifications) - [Working with notifications](notifications.md)
- [Advanced "recipes"](/recipes) - [Advanced "recipes"](../recipes/_index.md)
- [Registry API](/spec/api) - [Registry API](../spec/api.md)
- [Storage driver model](/storage-drivers) - [Storage driver model](../storage-drivers/_index.md)
- [Token authentication](/spec/auth/token) - [Token authentication](../spec/auth/token.md)

View file

@ -21,15 +21,15 @@ that certain layers no longer exist on the filesystem.
Filesystem layers are stored by their content address in the Registry. This Filesystem layers are stored by their content address in the Registry. This
has many advantages, one of which is that data is stored once and referred to by manifests. has many advantages, one of which is that data is stored once and referred to by manifests.
See [here](../compatibility#content-addressable-storage-cas) for more details. See [here](compatibility.md#content-addressable-storage-cas) for more details.
Layers are therefore shared amongst manifests; each manifest maintains a reference Layers are therefore shared amongst manifests; each manifest maintains a reference
to the layer. As long as a layer is referenced by one manifest, it cannot be garbage to the layer. As long as a layer is referenced by one manifest, it cannot be garbage
collected. collected.
Manifests and layers can be `deleted` with the registry API (refer to the API Manifests and layers can be `deleted` with the registry API (refer to the API
documentation [here](/spec/api#deleting-a-layer) and documentation [here](../spec/api.md#deleting-a-layer) and
[here](/spec/api#deleting-an-image) for details). This API removes references [here](../spec/api.md#deleting-an-image) for details). This API removes references
to the target and makes them eligible for garbage collection. It also makes them to the target and makes them eligible for garbage collection. It also makes them
unable to be read via the API. unable to be read via the API.

View file

@ -72,7 +72,7 @@ This is more secure than the insecure registry solution.
Be sure to use the name `myregistry.domain.com` as a CN. Be sure to use the name `myregistry.domain.com` as a CN.
2. Use the result to [start your registry with TLS enabled](../deploying#get-a-certificate). 2. Use the result to [start your registry with TLS enabled](deploying.md#get-a-certificate).
3. Instruct every Docker daemon to trust that certificate. The way to do this 3. Instruct every Docker daemon to trust that certificate. The way to do this
depends on your OS. depends on your OS.

View file

@ -10,7 +10,7 @@ pushes and pulls and layer pushes and pulls. These actions are serialized into
events. The events are queued into a registry-internal broadcast system which events. The events are queued into a registry-internal broadcast system which
queues and dispatches events to [_Endpoints_](#endpoints). queues and dispatches events to [_Endpoints_](#endpoints).
![Workflow of registry notifications](../../images/notifications.png) ![Workflow of registry notifications](/distribution/images/notifications.png)
## Endpoints ## Endpoints
@ -45,7 +45,7 @@ The above would configure the registry with an endpoint to send events to
5 failures happen consecutively, the registry backs off for 1 second before 5 failures happen consecutively, the registry backs off for 1 second before
trying again. trying again.
For details on the fields, see the [configuration documentation](../configuration/#notifications). For details on the fields, see the [configuration documentation](configuration.md#notifications).
A properly configured endpoint should lead to a log message from the registry A properly configured endpoint should lead to a log message from the registry
upon startup: upon startup:

View file

@ -12,7 +12,7 @@ Usually, that includes enterprise setups using LDAP/AD on the backend and a SSO
### Alternatives ### Alternatives
If you just want authentication for your registry, and are happy maintaining users access separately, you should really consider sticking with the native [basic auth registry feature](/about/deploying#native-basic-auth). If you just want authentication for your registry, and are happy maintaining users access separately, you should really consider sticking with the native [basic auth registry feature](../about/deploying.md#native-basic-auth).
### Solution ### Solution

View file

@ -38,7 +38,7 @@ The following table shows examples of allowed and disallowed mirror URLs.
> **Note** > **Note**
> >
> Mirrors of Docker Hub are still subject to Docker's [fair usage policy](https://www.docker.com/pricing/resource-consumption-updates){: target="blank" rel="noopener" class=“”}. > Mirrors of Docker Hub are still subject to Docker's [fair usage policy](https://www.docker.com/pricing/resource-consumption-updates).
### Solution ### Solution
@ -72,7 +72,7 @@ be configured to use the `filesystem` driver for storage.
The easiest way to run a registry as a pull through cache is to run the official The easiest way to run a registry as a pull through cache is to run the official
Registry image. Registry image.
At least, you need to specify `proxy.remoteurl` within `/etc/docker/registry/config.yml` At least, you need to specify `proxy.remoteurl` within `/etc/distribution/config.yml`
as described in the following subsection. as described in the following subsection.
Multiple registry caches can be deployed over the same back-end. A single Multiple registry caches can be deployed over the same back-end. A single
@ -107,7 +107,7 @@ proxy:
> **Warning**: For the scheduler to clean up old entries, `delete` must > **Warning**: For the scheduler to clean up old entries, `delete` must
> be enabled in the registry configuration. See > be enabled in the registry configuration. See
> [Registry Configuration](/about/configuration) for more details. > [Registry Configuration](../about/configuration.md) for more details.
### Configure the Docker daemon ### Configure the Docker daemon

View file

@ -17,7 +17,7 @@ mechanism fronting their internal http portal.
If you just want authentication for your registry, and are happy maintaining If you just want authentication for your registry, and are happy maintaining
users access separately, you should really consider sticking with the native users access separately, you should really consider sticking with the native
[basic auth registry feature](/about/deploying#native-basic-auth). [basic auth registry feature](../about/deploying.md#native-basic-auth).
### Solution ### Solution

View file

@ -6,7 +6,7 @@ keywords: registry, service, images, repository, json
# Docker Registry Reference # Docker Registry Reference
* [HTTP API V2](api) * [HTTP API V2](api.md)
* [Storage Driver](/storage-drivers/) * [Storage Driver](../storage-drivers/_index.md)
* [Token Authentication Specification](auth/token) * [Token Authentication Specification](auth/token.md)
* [Token Authentication Implementation](auth/jwt) * [Token Authentication Implementation](auth/jwt.md)

View file

@ -416,7 +416,7 @@ reference may include a tag or digest.
The client should include an Accept header indicating which manifest content The client should include an Accept header indicating which manifest content
types it supports. For more details on the manifest format and content types, types it supports. For more details on the manifest format and content types,
see [Image Manifest Version 2, Schema 2](../manifest-v2-2). see [Image Manifest Version 2, Schema 2](manifest-v2-2.md).
In a successful response, the Content-Type header will indicate which manifest type is being returned. In a successful response, the Content-Type header will indicate which manifest type is being returned.
A `404 Not Found` response will be returned if the image is unknown to the A `404 Not Found` response will be returned if the image is unknown to the
@ -840,7 +840,7 @@ Content-Type: <manifest media type>
The `name` and `reference` fields of the response body must match those The `name` and `reference` fields of the response body must match those
specified in the URL. The `reference` field may be a "tag" or a "digest". The specified in the URL. The `reference` field may be a "tag" or a "digest". The
content type should match the type of the manifest being uploaded, as specified content type should match the type of the manifest being uploaded, as specified
in [Image Manifest Version 2, Schema 2](../manifest-v2-2). in [Image Manifest Version 2, Schema 2](manifest-v2-2.md).
If there is a problem with pushing the manifest, a relevant 4xx response will If there is a problem with pushing the manifest, a relevant 4xx response will
be returned with a JSON error message. Please see the be returned with a JSON error message. Please see the
@ -1088,7 +1088,7 @@ response will be issued instead.
Accept: application/vnd.docker.distribution.manifest.v2+json Accept: application/vnd.docker.distribution.manifest.v2+json
> for more details, see: [compatibility](/about/compatibility#content-addressable-storage-cas) > for more details, see: [compatibility](../about/compatibility.md#content-addressable-storage-cas)
## Detail ## Detail

View file

@ -12,7 +12,7 @@ reference for the protocol and HTTP endpoints described here.
**Note**: Not all token servers implement oauth2. If the request to the endpoint **Note**: Not all token servers implement oauth2. If the request to the endpoint
returns `404` using the HTTP `POST` method, refer to returns `404` using the HTTP `POST` method, refer to
[Token Documentation](../token) for using the HTTP `GET` method supported by all [Token Documentation](token.md) for using the HTTP `GET` method supported by all
token servers. token servers.
## Refresh token format ## Refresh token format

View file

@ -144,7 +144,7 @@ Each JWT access token may only have a single subject and audience but multiple
resource scopes. The subject and audience are put into standard JWT fields resource scopes. The subject and audience are put into standard JWT fields
`sub` and `aud`. The resource scope is put into the `access` field. The `sub` and `aud`. The resource scope is put into the `access` field. The
structure of the access field can be seen in the structure of the access field can be seen in the
[jwt documentation](../jwt). [jwt documentation](jwt.md).
## Refresh Tokens ## Refresh Tokens

View file

@ -8,7 +8,7 @@ keywords: registry, on-prem, images, tags, repository, distribution, Bearer auth
This document outlines the v2 Distribution registry authentication scheme: This document outlines the v2 Distribution registry authentication scheme:
![v2 registry auth](../../../images/v2-registry-auth.png) ![v2 registry auth](/distribution/images/v2-registry-auth.png)
1. Attempt to begin a push/pull operation with the registry. 1. Attempt to begin a push/pull operation with the registry.
2. If the registry requires authorization it will return a `401 Unauthorized` 2. If the registry requires authorization it will return a `401 Unauthorized`
@ -188,7 +188,7 @@ https://auth.docker.io/token?service=registry.docker.io&scope=repository:samalba
The token server should first attempt to authenticate the client using any The token server should first attempt to authenticate the client using any
authentication credentials provided with the request. From Docker 1.11 the authentication credentials provided with the request. From Docker 1.11 the
Docker engine supports both Basic Authentication and [OAuth2](../oauth) for Docker engine supports both Basic Authentication and [OAuth2](oauth.md) for
getting tokens. Docker 1.10 and before, the registry client in the Docker Engine getting tokens. Docker 1.10 and before, the registry client in the Docker Engine
only supports Basic Authentication. If an attempt to authenticate to the token only supports Basic Authentication. If an attempt to authenticate to the token
server fails, the token server should return a `401 Unauthorized` response server fails, the token server should return a `401 Unauthorized` response

View file

@ -71,7 +71,7 @@ image manifest based on the Content-Type returned in the HTTP response.
- **`digest`** *string* - **`digest`** *string*
The digest of the content, as defined by the The digest of the content, as defined by the
[Registry V2 HTTP API Specificiation](../api#digest-parameter). [Registry V2 HTTP API Specification](api.md#digest-parameter).
- **`platform`** *object* - **`platform`** *object*
@ -187,7 +187,7 @@ image. It's the direct replacement for the schema-1 manifest.
- **`digest`** *string* - **`digest`** *string*
The digest of the content, as defined by the The digest of the content, as defined by the
[Registry V2 HTTP API Specificiation](../api#digest-parameter). [Registry V2 HTTP API Specification](api.md#digest-parameter).
- **`layers`** *array* - **`layers`** *array*
@ -213,7 +213,7 @@ image. It's the direct replacement for the schema-1 manifest.
- **`digest`** *string* - **`digest`** *string*
The digest of the content, as defined by the The digest of the content, as defined by the
[Registry V2 HTTP API Specificiation](../api#digest-parameter). [Registry V2 HTTP API Specification](api.md#digest-parameter).
- **`urls`** *array* - **`urls`** *array*

View file

@ -20,7 +20,22 @@ An implementation of the `storagedriver.StorageDriver` interface which uses [Mic
## Related information ## Related information
* To get information about * To get information about Azure blob storage [the offical docs](https://azure.microsoft.com/en-us/services/storage/).
[azure-blob-storage](https://azure.microsoft.com/en-us/services/storage/), visit * You can use Azure [Blob Service REST API](https://docs.microsoft.com/en-us/rest/api/storageservices/Blob-Service-REST-API) to [create a storage container](https://docs.microsoft.com/en-us/rest/api/storageservices/Create-Container).
the Microsoft website.
* You can use Microsoft's [Blob Service REST API](https://docs.microsoft.com/en-us/rest/api/storageservices/Blob-Service-REST-API) to [create a storage container](https://docs.microsoft.com/en-us/rest/api/storageservices/Create-Container). ## Azure identity
In order to use managed identity to access Azure blob storage you can use [Microsoft Bicep](https://learn.microsoft.com/en-us/azure/templates/microsoft.app/managedenvironments/storages?pivots=deployment-language-bicep).
The following will configure credentials that will be used by the Azure storage driver to construct AZ Identity that will be used to access the blob storage:
```
properties: {
azure: {
accountname: accountname
container: containername
credentials: {
type: default
}
}
}
```

View file

@ -17,4 +17,8 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Goog
{{< hint type=note >}} {{< hint type=note >}}
Instead of a key file you can use [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials). Instead of a key file you can use [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials).
To use redirects with default credentials assigned to a virtual machine you have to enable "IAM Service Account Credentials API" and grant `iam.serviceAccounts.signBlob` permission on the used service account.
To use redirects with default credentials from Google Cloud CLI, in addition to the permissions mentioned above, you have to [impersonate the service account intended to be used by the registry](https://cloud.google.com/sdk/gcloud/reference#--impersonate-service-account).
{{< /hint >}} {{< /hint >}}

View file

@ -7,7 +7,7 @@ title: In-memory storage driver (testing only)
For purely tests purposes, you can use the `inmemory` storage driver. This For purely tests purposes, you can use the `inmemory` storage driver. This
driver is an implementation of the `storagedriver.StorageDriver` interface which driver is an implementation of the `storagedriver.StorageDriver` interface which
uses local memory for object storage. If you would like to run a registry from uses local memory for object storage. If you would like to run a registry from
volatile memory, use the [`filesystem` driver](../filesystem) on a ramdisk. volatile memory, use the [`filesystem` driver](filesystem.md) on a ramdisk.
{{< hint type=important >}} {{< hint type=important >}}
This storage driver *does not* persist data across runs. This is why it is only suitable for testing. *Never* use this driver in production. This storage driver *does not* persist data across runs. This is why it is only suitable for testing. *Never* use this driver in production.

View file

@ -0,0 +1,15 @@
---
description: Explains how to use storage middleware
keywords: registry, on-prem, images, tags, repository, distribution, storage drivers, advanced
title: Storage middleware
---
This document describes the registry storage middleware.
## Provided middleware
This storage driver package comes bundled with several middleware options:
- cloudfront
- redirect
- [rewrite](rewrite): Partially rewrites the URL returned by the storage driver.

View file

@ -0,0 +1,32 @@
---
description: Explains how to use the rewrite storage middleware
keywords: registry, service, driver, images, storage, middleware, rewrite
title: Rewrite middleware
---
A storage middleware which allows to rewrite the URL returned by the storage driver.
For example, it can be used to rewrite the Blob Storage URL returned by the Azure Blob Storage driver to use Azure CDN.
## Parameters
* `scheme`: (optional): Rewrite the returned URL scheme (if set).
* `host`: (optional): Rewrite the returned URL host (if set).
* `trimpathprefix` (optional): Trim the prefix from the returned URL path (if set).
## Example configuration
```yaml
storage:
azure:
accountname: "ACCOUNT_NAME"
accountkey: "******"
container: container-name
middleware:
storage:
- name: rewrite
options:
scheme: https
host: example-cdn-endpoint.azurefd.net
trimpathprefix: /container-name
```

View file

@ -15,7 +15,7 @@ Amazon S3 or S3 compatible services for object storage.
| `secretkey` | no | Your AWS Secret Key. If you use [IAM roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html), omit to fetch temporary credentials from IAM. | | `secretkey` | no | Your AWS Secret Key. If you use [IAM roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html), omit to fetch temporary credentials from IAM. |
| `region` | yes | The AWS region in which your bucket exists. | | `region` | yes | The AWS region in which your bucket exists. |
| `regionendpoint` | no | Endpoint for S3 compatible storage services (Minio, etc). | | `regionendpoint` | no | Endpoint for S3 compatible storage services (Minio, etc). |
| `forcepathstyle` | no | To enable path-style addressing when the value is set to `true`. The default is `true`. | | `forcepathstyle` | no | To enable path-style addressing when the value is set to `true`. The default is `false`. |
| `bucket` | yes | The bucket name in which you want to store the registry's data. | | `bucket` | yes | The bucket name in which you want to store the registry's data. |
| `encrypt` | no | Specifies whether the registry stores the image in encrypted format or not. A boolean value. The default is `false`. | | `encrypt` | no | Specifies whether the registry stores the image in encrypted format or not. A boolean value. The default is `false`. |
| `keyid` | no | Optional KMS key ID to use for encryption (encrypt must be true, or this parameter is ignored). The default is `none`. | | `keyid` | no | Optional KMS key ID to use for encryption (encrypt must be true, or this parameter is ignored). The default is `none`. |
@ -43,7 +43,7 @@ Amazon S3 or S3 compatible services for object storage.
`regionendpoint`: (optional) Endpoint URL for S3 compatible APIs. This should not be provided when using Amazon S3. `regionendpoint`: (optional) Endpoint URL for S3 compatible APIs. This should not be provided when using Amazon S3.
`forcepathstyle`: (optional) The force path style for S3 compatible APIs. Some manufacturers only support force path style, while others only support DNS based bucket routing. Amazon S3 supports both. `forcepathstyle`: (optional) Force path style for S3 compatible APIs. Some manufacturers only support force path style, while others only support DNS based bucket routing. Amazon S3 supports both. The value of this parameter applies, regardless of the region settings.
`bucket`: The name of your S3 bucket where you wish to store objects. The bucket must exist prior to the driver initialization. `bucket`: The name of your S3 bucket where you wish to store objects. The bucket must exist prior to the driver initialization.

View file

@ -5,7 +5,7 @@ This repository provides container images for the Open Source Registry implement
<img src="https://raw.githubusercontent.com/distribution/distribution/main/distribution-logo.svg" width="200px" /> <img src="https://raw.githubusercontent.com/distribution/distribution/main/distribution-logo.svg" width="200px" />
[![Build Status](https://github.com/distribution/distribution/workflows/CI/badge.svg?branch=main&event=push)](https://github.com/distribution/distribution/actions?query=workflow%3ACI) [![Build Status](https://github.com/distribution/distribution/workflows/build/badge.svg?branch=main&event=push)](https://github.com/distribution/distribution/actions/workflows/build.yml?query=workflow%3Abuild)
[![OCI Conformance](https://github.com/distribution/distribution/workflows/conformance/badge.svg)](https://github.com/distribution/distribution/actions?query=workflow%3Aconformance) [![OCI Conformance](https://github.com/distribution/distribution/workflows/conformance/badge.svg)](https://github.com/distribution/distribution/actions?query=workflow%3Aconformance)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE) [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)
@ -31,12 +31,12 @@ docker tag alpine localhost:5000/alpine
docker push localhost:5000/alpine docker push localhost:5000/alpine
``` ```
⚠️ Beware the default configuration uses [`filesystem` storage driver](https://github.com/distribution/distribution/blob/main/docs/storage-drivers/filesystem.md) ⚠️ Beware the default configuration uses [`filesystem` storage driver](https://github.com/distribution/distribution/blob/main/docs/content/storage-drivers/filesystem.md)
and the above example command does not mount a local filesystem volume into the running container. and the above example command does not mount a local filesystem volume into the running container.
If you wish to mount the local filesystem to the `rootdirectory` of the If you wish to mount the local filesystem to the `rootdirectory` of the
`filesystem` storage driver run the following command: `filesystem` storage driver run the following command:
``` ```
docker run -d -p 5000:5000 $PWD/FS/PATH:/var/lib/registry --restart always --name registry distribution/distribution:edge docker run -d -p 5000:5000 -v $PWD/FS/PATH:/var/lib/registry --restart always --name registry distribution/distribution:edge
``` ```
### Custom configuration ### Custom configuration
@ -44,7 +44,7 @@ docker run -d -p 5000:5000 $PWD/FS/PATH:/var/lib/registry --restart always --nam
If you don't wan to use the default configuration file, you can supply If you don't wan to use the default configuration file, you can supply
your own custom configuration file as follows: your own custom configuration file as follows:
``` ```
docker run -d -p 5000:5000 $PWD/PATH/TO/config.yml:/etc/docker/registry/config.yml --restart always --name registry distribution/distribution:edge docker run -d -p 5000:5000 -v $PWD/PATH/TO/config.yml:/etc/distribution/config.yml --restart always --name registry distribution/distribution:edge
``` ```
## Communication ## Communication

View file

@ -1,4 +1,4 @@
baseURL: / baseURL: https://distribution.github.io/distribution
languageCode: en-us languageCode: en-us
title: CNCF Distribution title: CNCF Distribution
theme: hugo-geekdoc theme: hugo-geekdoc
@ -22,3 +22,7 @@ disablePathToLower: true
params: params:
geekdocRepo: "https://github.com/distribution/distribution" geekdocRepo: "https://github.com/distribution/distribution"
geekdocEditPath: edit/main/docs geekdocEditPath: edit/main/docs
geekdocLegalNotice: "https://www.linuxfoundation.org/legal/trademark-usage"
geekdocContentLicense:
name: CC BY 4.0
link: https://creativecommons.org/licenses/by/4.0/

1
docs/i18n/en.yaml Normal file
View file

@ -0,0 +1 @@
footer_legal_notice: Trademarks

View file

@ -0,0 +1,5 @@
{{- if (strings.HasPrefix .Destination "http") -}}
<a href="{{ safe.URL .Destination }}" target="_blank">{{ safe.HTML .Text }}</a>
{{- else -}}
<a href="{{ ref .Page .Destination | safe.URL }}">{{ safe.HTML .Text }}</a>
{{- end -}}

134
go.mod
View file

@ -5,8 +5,8 @@ go 1.21
toolchain go1.21.4 toolchain go1.21.4
require ( require (
cloud.google.com/go/storage v1.30.1 cloud.google.com/go/storage v1.36.0
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141532-e5040d35e99d git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240716083621-e18b91623138
git.frostfs.info/TrueCloudLab/tzhash v1.8.0 git.frostfs.info/TrueCloudLab/tzhash v1.8.0
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1
@ -15,17 +15,17 @@ require (
github.com/aws/aws-sdk-go v1.48.10 github.com/aws/aws-sdk-go v1.48.10
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 github.com/bshuster-repo/logrus-logstash-hook v1.0.0
github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/go-systemd/v22 v22.5.0
github.com/distribution/reference v0.5.0 github.com/distribution/reference v0.6.0
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c
github.com/docker/go-metrics v0.0.1 github.com/docker/go-metrics v0.0.1
github.com/go-jose/go-jose/v3 v3.0.1 github.com/go-jose/go-jose/v4 v4.0.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.2
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/hashicorp/golang-lru/arc/v2 v2.0.5 github.com/hashicorp/golang-lru/arc/v2 v2.0.5
github.com/klauspost/compress v1.17.4 github.com/klauspost/compress v1.17.4
github.com/mitchellh/mapstructure v1.1.2 github.com/mitchellh/mapstructure v1.5.0
github.com/nspcc-dev/neo-go v0.101.2-0.20230601131642-a0117042e8fc github.com/nspcc-dev/neo-go v0.106.2
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0 github.com/opencontainers/image-spec v1.1.0
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 github.com/redis/go-redis/extra/redisotel/v9 v9.0.5
@ -35,23 +35,29 @@ require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.29.1 github.com/testcontainers/testcontainers-go v0.29.1
go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 go.opentelemetry.io/contrib/exporters/autoexport v0.46.1
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0
go.opentelemetry.io/otel v1.22.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0 go.opentelemetry.io/otel/sdk v1.21.0
go.opentelemetry.io/otel/trace v1.22.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.24.0 golang.org/x/crypto v0.24.0
golang.org/x/oauth2 v0.11.0 golang.org/x/net v0.26.0
google.golang.org/api v0.126.0 golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.7.0
google.golang.org/api v0.162.0
google.golang.org/grpc v1.62.0
gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v2 v2.4.0
) )
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
require ( require (
cloud.google.com/go v0.110.7 // indirect cloud.google.com/go v0.112.0 // indirect
cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute v1.24.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/iam v1.1.1 // indirect cloud.google.com/go/iam v1.1.6 // indirect
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240327095603-491a47e7fe24 // indirect git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3 // indirect
git.frostfs.info/TrueCloudLab/frostfs-contract v0.0.0-20230307110621-19a8ef2d02fb // indirect git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3 // indirect
git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect
git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect
git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect
@ -61,23 +67,37 @@ require (
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/containerd v1.7.12 // indirect github.com/containerd/containerd v1.7.12 // indirect
github.com/containerd/log v0.1.0 // indirect github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/docker v25.0.3+incompatible // indirect github.com/docker/docker v25.0.3+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/google/s2a-go v0.1.7 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
@ -86,54 +106,31 @@ require (
github.com/moby/term v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect
github.com/nspcc-dev/go-ordered-json v0.0.0-20220111165707-25110be27d22 // indirect github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect
github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20230615193820-9185820289ce // indirect github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20240521091047-78685785716d // indirect
github.com/nspcc-dev/rfc6979 v0.2.0 // indirect github.com/nspcc-dev/rfc6979 v0.2.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.19.0 // indirect; updated to latest
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.1 // indirect
github.com/twmb/murmur3 v1.1.8 // indirect github.com/twmb/murmur3 v1.1.8 // indirect
github.com/urfave/cli v1.22.12 // indirect github.com/urfave/cli v1.22.12 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.uber.org/atomic v1.10.0 // indirect go.etcd.io/bbolt v1.3.9 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect; updated to latest
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
@ -141,21 +138,20 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/net v0.26.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.21.0 // indirect golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect golang.org/x/text v0.16.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect
google.golang.org/grpc v1.59.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

670
go.sum

File diff suppressed because it is too large Load diff

View file

@ -72,7 +72,7 @@ type Manager interface {
// AddResponse adds the response to the challenge // AddResponse adds the response to the challenge
// manager. The challenges will be parsed out of // manager. The challenges will be parsed out of
// the WWW-Authenicate headers and added to the // the WWW-Authenticate headers and added to the
// URL which was produced the response. If the // URL which was produced the response. If the
// response was authorized, any challenges for the // response was authorized, any challenges for the
// endpoint will be cleared. // endpoint will be cleared.

View file

@ -29,9 +29,9 @@ var (
const defaultClientID = "registry-client" const defaultClientID = "registry-client"
// AuthenticationHandler is an interface for authorizing a request from // AuthenticationHandler is an interface for authorizing a request from
// params from a "WWW-Authenicate" header for a single scheme. // params from a "WWW-Authenticate" header for a single scheme.
type AuthenticationHandler interface { type AuthenticationHandler interface {
// Scheme returns the scheme as expected from the "WWW-Authenicate" header. // Scheme returns the scheme as expected from the "WWW-Authenticate" header.
Scheme() string Scheme() string
// AuthorizeRequest adds the authorization header to a request (if needed) // AuthorizeRequest adds the authorization header to a request (if needed)

View file

@ -46,8 +46,14 @@ func parseHTTPErrorResponse(resp *http.Response) error {
} }
statusCode := resp.StatusCode statusCode := resp.StatusCode
ctHeader := resp.Header.Get("Content-Type")
// A HEAD request for example validly does not contain any body, while
// still returning a JSON content-type.
if len(body) == 0 {
return makeError(statusCode, "")
}
ctHeader := resp.Header.Get("Content-Type")
if ctHeader == "" { if ctHeader == "" {
return makeError(statusCode, string(body)) return makeError(statusCode, string(body))
} }

View file

@ -57,6 +57,22 @@ func TestHandleHTTPResponseError401WithInvalidBody(t *testing.T) {
} }
} }
func TestHandleHTTPResponseError401WithNoBody(t *testing.T) {
json := ""
response := &http.Response{
Status: "401 Unauthorized",
StatusCode: 401,
Body: nopCloser{bytes.NewBufferString(json)},
Header: http.Header{"Content-Type": []string{"application/json; charset=utf-8"}},
}
err := HandleHTTPResponseError(response)
expectedMsg := "unauthorized: "
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected %q, got: %q", expectedMsg, err.Error())
}
}
func TestHandleHTTPResponseErrorExpectedStatusCode400ValidBody(t *testing.T) { func TestHandleHTTPResponseErrorExpectedStatusCode400ValidBody(t *testing.T) {
json := `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest does not match"}]}` json := `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest does not match"}]}`
response := &http.Response{ response := &http.Response{

View file

@ -26,11 +26,6 @@ var (
ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request") ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request")
) )
// ReadSeekCloser combines io.ReadSeeker with io.Closer.
//
// Deprecated: use [io.ReadSeekCloser].
type ReadSeekCloser = io.ReadSeekCloser
// NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET // NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET
// request. When seeking and starting a read from a non-zero offset // request. When seeking and starting a read from a non-zero offset
// the a "Range" header will be added which sets the offset. // the a "Range" header will be added which sets the offset.

View file

@ -47,7 +47,7 @@ type ManifestBuilder interface {
AppendReference(dependency Describable) error AppendReference(dependency Describable) error
} }
// ManifestService describes operations on image manifests. // ManifestService describes operations on manifests.
type ManifestService interface { type ManifestService interface {
// Exists returns true if the manifest exists. // Exists returns true if the manifest exists.
Exists(ctx context.Context, dgst digest.Digest) (bool, error) Exists(ctx context.Context, dgst digest.Digest) (bool, error)

View file

@ -269,7 +269,7 @@ type RouteDescriptor struct {
// should match. // should match.
Path string Path string
// Entity should be a short, human-readalbe description of the object // Entity should be a short, human-readable description of the object
// targeted by the endpoint. // targeted by the endpoint.
Entity string Entity string

View file

@ -202,7 +202,7 @@ func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string,
return appendValuesURL(uploadURL, values...).String(), nil return appendValuesURL(uploadURL, values...).String(), nil
} }
// clondedRoute returns a clone of the named route from the router. Routes // cloneRoute returns a clone of the named route from the router. Routes
// must be cloned to avoid modifying them during url generation. // must be cloned to avoid modifying them during url generation.
func (ub *URLBuilder) cloneRoute(name string) clonedRoute { func (ub *URLBuilder) cloneRoute(name string) clonedRoute {
route := new(mux.Route) route := new(mux.Route)

View file

@ -46,7 +46,7 @@ var (
) )
// InitFunc is the type of an AccessController factory function and is used // InitFunc is the type of an AccessController factory function and is used
// to register the constructor for different AccesController backends. // to register the constructor for different AccessController backends.
type InitFunc func(options map[string]interface{}) (AccessController, error) type InitFunc func(options map[string]interface{}) (AccessController, error)
var accessControllers map[string]InitFunc var accessControllers map[string]InitFunc
@ -56,7 +56,7 @@ func init() {
} }
// UserInfo carries information about // UserInfo carries information about
// an autenticated/authorized client. // an authenticated/authorized client.
type UserInfo struct { type UserInfo struct {
Name string Name string
} }

View file

@ -9,11 +9,12 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
"github.com/distribution/distribution/v3/registry/auth" "github.com/distribution/distribution/v3/registry/auth"
"github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v4"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -86,6 +87,7 @@ type authChallenge struct {
err error err error
realm string realm string
autoRedirect bool autoRedirect bool
autoRedirectPath string
service string service string
accessSet accessSet accessSet accessSet
} }
@ -102,13 +104,28 @@ func (ac authChallenge) Status() int {
return http.StatusUnauthorized return http.StatusUnauthorized
} }
func buildAutoRedirectURL(r *http.Request, autoRedirectPath string) string {
scheme := "https"
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
scheme = forwardedProto
}
u := &url.URL{
Scheme: scheme,
Host: r.Host,
Path: autoRedirectPath,
}
return u.String()
}
// challengeParams constructs the value to be used in // challengeParams constructs the value to be used in
// the WWW-Authenticate response challenge header. // the WWW-Authenticate response challenge header.
// See https://tools.ietf.org/html/rfc6750#section-3 // See https://tools.ietf.org/html/rfc6750#section-3
func (ac authChallenge) challengeParams(r *http.Request) string { func (ac authChallenge) challengeParams(r *http.Request) string {
var realm string var realm string
if ac.autoRedirect { if ac.autoRedirect {
realm = fmt.Sprintf("https://%s/auth/token", r.Host) realm = buildAutoRedirectURL(r, ac.autoRedirectPath)
} else { } else {
realm = ac.realm realm = ac.realm
} }
@ -127,7 +144,7 @@ func (ac authChallenge) challengeParams(r *http.Request) string {
return str return str
} }
// SetChallenge sets the WWW-Authenticate value for the response. // SetHeaders sets the WWW-Authenticate value for the response.
func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) { func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
w.Header().Add("WWW-Authenticate", ac.challengeParams(r)) w.Header().Add("WWW-Authenticate", ac.challengeParams(r))
} }
@ -136,21 +153,29 @@ func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
type accessController struct { type accessController struct {
realm string realm string
autoRedirect bool autoRedirect bool
autoRedirectPath string
issuer string issuer string
service string service string
rootCerts *x509.CertPool rootCerts *x509.CertPool
trustedKeys map[string]crypto.PublicKey trustedKeys map[string]crypto.PublicKey
signingAlgorithms []jose.SignatureAlgorithm
} }
const (
defaultAutoRedirectPath = "/auth/token"
)
// tokenAccessOptions is a convenience type for handling // tokenAccessOptions is a convenience type for handling
// options to the contstructor of an accessController. // options to the constructor of an accessController.
type tokenAccessOptions struct { type tokenAccessOptions struct {
realm string realm string
autoRedirect bool autoRedirect bool
autoRedirectPath string
issuer string issuer string
service string service string
rootCertBundle string rootCertBundle string
jwks string jwks string
signingAlgorithms []string
} }
// checkOptions gathers the necessary options // checkOptions gathers the necessary options
@ -183,10 +208,32 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
if ok { if ok {
autoRedirect, ok := autoRedirectVal.(bool) autoRedirect, ok := autoRedirectVal.(bool)
if !ok { if !ok {
return opts, fmt.Errorf("token auth requires a valid option bool: autoredirect") return opts, errors.New("token auth requires a valid option bool: autoredirect")
} }
opts.autoRedirect = autoRedirect opts.autoRedirect = autoRedirect
} }
if opts.autoRedirect {
autoRedirectPathVal, ok := options["autoredirectpath"]
if ok {
autoRedirectPath, ok := autoRedirectPathVal.(string)
if !ok {
return opts, errors.New("token auth requires a valid option string: autoredirectpath")
}
opts.autoRedirectPath = autoRedirectPath
}
if opts.autoRedirectPath == "" {
opts.autoRedirectPath = defaultAutoRedirectPath
}
}
signingAlgos, ok := options["signingalgorithms"]
if ok {
signingAlgorithmsVals, ok := signingAlgos.([]string)
if !ok {
return opts, errors.New("signingalgorithms must be a list of signing algorithms")
}
opts.signingAlgorithms = signingAlgorithmsVals
}
return opts, nil return opts, nil
} }
@ -243,6 +290,18 @@ func getJwks(path string) (*jose.JSONWebKeySet, error) {
return &jwks, nil return &jwks, nil
} }
func getSigningAlgorithms(algos []string) ([]jose.SignatureAlgorithm, error) {
signAlgVals := make([]jose.SignatureAlgorithm, 0, len(algos))
for _, alg := range algos {
alg, ok := signingAlgorithms[alg]
if !ok {
return nil, fmt.Errorf("unsupported signing algorithm: %s", alg)
}
signAlgVals = append(signAlgVals, alg)
}
return signAlgVals, nil
}
// newAccessController creates an accessController using the given options. // newAccessController creates an accessController using the given options.
func newAccessController(options map[string]interface{}) (auth.AccessController, error) { func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
config, err := checkOptions(options) config, err := checkOptions(options)
@ -253,6 +312,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
var ( var (
rootCerts []*x509.Certificate rootCerts []*x509.Certificate
jwks *jose.JSONWebKeySet jwks *jose.JSONWebKeySet
signAlgos []jose.SignatureAlgorithm
) )
if config.rootCertBundle != "" { if config.rootCertBundle != "" {
@ -286,13 +346,25 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
} }
} }
signAlgos, err = getSigningAlgorithms(config.signingAlgorithms)
if err != nil {
return nil, err
}
if len(signAlgos) == 0 {
// NOTE: this is to maintain backwards compat
// with existing registry deployments
signAlgos = defaultSigningAlgorithms
}
return &accessController{ return &accessController{
realm: config.realm, realm: config.realm,
autoRedirect: config.autoRedirect, autoRedirect: config.autoRedirect,
autoRedirectPath: config.autoRedirectPath,
issuer: config.issuer, issuer: config.issuer,
service: config.service, service: config.service,
rootCerts: rootPool, rootCerts: rootPool,
trustedKeys: trustedKeys, trustedKeys: trustedKeys,
signingAlgorithms: signAlgos,
}, nil }, nil
} }
@ -302,6 +374,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
challenge := &authChallenge{ challenge := &authChallenge{
realm: ac.realm, realm: ac.realm,
autoRedirect: ac.autoRedirect, autoRedirect: ac.autoRedirect,
autoRedirectPath: ac.autoRedirectPath,
service: ac.service, service: ac.service,
accessSet: newAccessSet(accessItems...), accessSet: newAccessSet(accessItems...),
} }
@ -312,7 +385,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
return nil, challenge return nil, challenge
} }
token, err := NewToken(rawToken) token, err := NewToken(rawToken, ac.signingAlgorithms)
if err != nil { if err != nil {
challenge.err = err challenge.err = err
return nil, challenge return nil, challenge

View file

@ -0,0 +1,89 @@
package token
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestBuildAutoRedirectURL(t *testing.T) {
cases := []struct {
name string
reqGetter func() *http.Request
autoRedirectPath string
expectedURL string
}{{
name: "http",
reqGetter: func() *http.Request {
req := httptest.NewRequest("GET", "http://example.com/", nil)
return req
},
autoRedirectPath: "/auth",
expectedURL: "https://example.com/auth",
}, {
name: "x-forwarded",
reqGetter: func() *http.Request {
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.Header.Set("X-Forwarded-Proto", "http")
return req
},
autoRedirectPath: "/auth/token",
expectedURL: "http://example.com/auth/token",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := tc.reqGetter()
result := buildAutoRedirectURL(req, tc.autoRedirectPath)
if result != tc.expectedURL {
t.Errorf("expected %s, got %s", tc.expectedURL, result)
}
})
}
}
func TestCheckOptions(t *testing.T) {
realm := "https://auth.example.com/token/"
issuer := "test-issuer.example.com"
service := "test-service.example.com"
options := map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"rootcertbundle": "",
"autoredirect": true,
"autoredirectpath": "/auth",
}
ta, err := checkOptions(options)
if err != nil {
t.Fatal(err)
}
if ta.autoRedirect != true {
t.Fatal("autoredirect should be true")
}
if ta.autoRedirectPath != "/auth" {
t.Fatal("autoredirectpath should be /auth")
}
options = map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"rootcertbundle": "",
"autoredirect": true,
"autoredirectforcetlsdisabled": true,
}
ta, err = checkOptions(options)
if err != nil {
t.Fatal(err)
}
if ta.autoRedirect != true {
t.Fatal("autoredirect should be true")
}
if ta.autoRedirectPath != "/auth/token" {
t.Fatal("autoredirectpath should be /auth/token")
}
}

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
fuzz "github.com/AdaLogics/go-fuzz-headers" fuzz "github.com/AdaLogics/go-fuzz-headers"
"github.com/go-jose/go-jose/v4"
) )
func FuzzToken1(f *testing.F) { func FuzzToken1(f *testing.F) {
@ -18,7 +19,7 @@ func FuzzToken1(f *testing.F) {
if err != nil { if err != nil {
return return
} }
token, err := NewToken(rawToken) token, err := NewToken(rawToken, []jose.SignatureAlgorithm{jose.EdDSA, jose.RS384})
if err != nil { if err != nil {
return return
} }

View file

@ -7,8 +7,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v3/jwt" "github.com/go-jose/go-jose/v4/jwt"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/distribution/distribution/v3/registry/auth" "github.com/distribution/distribution/v3/registry/auth"
@ -23,6 +23,38 @@ const (
Leeway = 60 * time.Second Leeway = 60 * time.Second
) )
var signingAlgorithms = map[string]jose.SignatureAlgorithm{
"EdDSA": jose.EdDSA,
"HS256": jose.HS256,
"HS384": jose.HS384,
"HS512": jose.HS512,
"RS256": jose.RS256,
"RS384": jose.RS384,
"RS512": jose.RS512,
"ES256": jose.ES256,
"ES384": jose.ES384,
"ES512": jose.ES512,
"PS256": jose.PS256,
"PS384": jose.PS384,
"PS512": jose.PS512,
}
var defaultSigningAlgorithms = []jose.SignatureAlgorithm{
jose.EdDSA,
jose.HS256,
jose.HS384,
jose.HS512,
jose.RS256,
jose.RS384,
jose.RS512,
jose.ES256,
jose.ES384,
jose.ES512,
jose.PS256,
jose.PS384,
jose.PS512,
}
// Errors used by token parsing and verification. // Errors used by token parsing and verification.
var ( var (
ErrMalformedToken = errors.New("malformed token") ErrMalformedToken = errors.New("malformed token")
@ -69,8 +101,8 @@ type VerifyOptions struct {
// NewToken parses the given raw token string // NewToken parses the given raw token string
// and constructs an unverified JSON Web Token. // and constructs an unverified JSON Web Token.
func NewToken(rawToken string) (*Token, error) { func NewToken(rawToken string, signingAlgs []jose.SignatureAlgorithm) (*Token, error) {
token, err := jwt.ParseSigned(rawToken) token, err := jwt.ParseSigned(rawToken, signingAlgs)
if err != nil { if err != nil {
return nil, ErrMalformedToken return nil, ErrMalformedToken
} }
@ -140,6 +172,13 @@ func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey crypto.Pu
// verifying the first one in the list only at the moment. // verifying the first one in the list only at the moment.
header := t.JWT.Headers[0] header := t.JWT.Headers[0]
signingKey, err = verifyCertChain(header, verifyOpts.Roots)
// NOTE(milosgajdos): if the x5c header is missing
// the token may have been signed by a JWKS.
if err != nil && err != jose.ErrMissingX5cHeader {
return
}
switch { switch {
case header.JSONWebKey != nil: case header.JSONWebKey != nil:
signingKey, err = verifyJWK(header, verifyOpts) signingKey, err = verifyJWK(header, verifyOpts)
@ -149,7 +188,7 @@ func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey crypto.Pu
err = fmt.Errorf("token signed by untrusted key with ID: %q", header.KeyID) err = fmt.Errorf("token signed by untrusted key with ID: %q", header.KeyID)
} }
default: default:
signingKey, err = verifyCertChain(header, verifyOpts.Roots) err = ErrInvalidToken
} }
return return
@ -226,7 +265,7 @@ func getCertPubKey(chains [][]*x509.Certificate) crypto.PublicKey {
// NOTE: we dont have to verify that the public key in the leaf cert // NOTE: we dont have to verify that the public key in the leaf cert
// *is* the signing key: if it's not the signing then token claims // *is* the signing key: if it's not the signing then token claims
// verifcation with this key fails // verification with this key fails
return cert.PublicKey.(crypto.PublicKey) return cert.PublicKey.(crypto.PublicKey)
} }

View file

@ -19,8 +19,8 @@ import (
"time" "time"
"github.com/distribution/distribution/v3/registry/auth" "github.com/distribution/distribution/v3/registry/auth"
"github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v3/jwt" "github.com/go-jose/go-jose/v4/jwt"
) )
func makeRootKeys(numKeys int) ([]*ecdsa.PrivateKey, error) { func makeRootKeys(numKeys int) ([]*ecdsa.PrivateKey, error) {
@ -123,12 +123,12 @@ func makeTestToken(jwk *jose.JSONWebKey, issuer, audience string, access []*Reso
Access: access, Access: access,
} }
tokenString, err := jwt.Signed(signer).Claims(claimSet).CompactSerialize() tokenString, err := jwt.Signed(signer).Claims(claimSet).Serialize()
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to build token string: %v", err) return nil, fmt.Errorf("unable to build token string: %v", err)
} }
return NewToken(tokenString) return NewToken(tokenString, []jose.SignatureAlgorithm{signingKey.Algorithm})
} }
// NOTE(milosgajdos): certTemplateInfo type as well // NOTE(milosgajdos): certTemplateInfo type as well

View file

@ -1709,6 +1709,33 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Name
// ------------------ // ------------------
// Fetch by tag name // Fetch by tag name
// HEAD requests should not contain a body
headReq, err := http.NewRequest(http.MethodHead, manifestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
headResp, err := http.DefaultClient.Do(headReq)
if err != nil {
t.Fatalf("unexpected error head manifest: %v", err)
}
defer headResp.Body.Close()
checkResponse(t, "head uploaded manifest", headResp, http.StatusOK)
checkHeaders(t, headResp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
headBody, err := io.ReadAll(headResp.Body)
if err != nil {
t.Fatalf("reading body for head manifest: %v", err)
}
if len(headBody) > 0 {
t.Fatalf("unexpected body length for head manifest: %d", len(headBody))
}
req, err := http.NewRequest(http.MethodGet, manifestURL, nil) req, err := http.NewRequest(http.MethodGet, manifestURL, nil)
if err != nil { if err != nil {
t.Fatalf("Error constructing request: %s", err) t.Fatalf("Error constructing request: %s", err)
@ -1744,6 +1771,32 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Name
// --------------- // ---------------
// Fetch by digest // Fetch by digest
// HEAD requests should not contain a body
headReq, err = http.NewRequest(http.MethodHead, manifestDigestURL, nil)
if err != nil {
t.Fatalf("Error constructing request: %s", err)
}
headResp, err = http.DefaultClient.Do(headReq)
if err != nil {
t.Fatalf("unexpected error head manifest: %v", err)
}
defer headResp.Body.Close()
checkResponse(t, "head uploaded manifest by digest", headResp, http.StatusOK)
checkHeaders(t, headResp, http.Header{
"Docker-Content-Digest": []string{dgst.String()},
"ETag": []string{fmt.Sprintf(`"%s"`, dgst)},
})
headBody, err = io.ReadAll(headResp.Body)
if err != nil {
t.Fatalf("reading body for head manifest by digest: %v", err)
}
if len(headBody) > 0 {
t.Fatalf("unexpected body length for head manifest: %d", len(headBody))
}
req, err = http.NewRequest(http.MethodGet, manifestDigestURL, nil) req, err = http.NewRequest(http.MethodGet, manifestDigestURL, nil)
if err != nil { if err != nil {
t.Fatalf("Error constructing request: %s", err) t.Fatalf("Error constructing request: %s", err)
@ -2461,7 +2514,7 @@ func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLB
func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) {
if resp.StatusCode != expectedStatus { if resp.StatusCode != expectedStatus {
t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) t.Logf("unexpected status %s: expected %v, got %v", msg, resp.StatusCode, expectedStatus)
maybeDumpResponse(t, resp) maybeDumpResponse(t, resp)
t.FailNow() t.FailNow()
} }
@ -2543,6 +2596,8 @@ func maybeDumpResponse(t *testing.T, resp *http.Response) {
// test will fail. If a passed in header value is "*", any non-zero value will // test will fail. If a passed in header value is "*", any non-zero value will
// suffice as a match. // suffice as a match.
func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) { func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) {
t.Helper()
for k, vs := range headers { for k, vs := range headers {
if resp.Header.Get(k) == "" { if resp.Header.Get(k) == "" {
t.Fatalf("response missing header %q", k) t.Fatalf("response missing header %q", k)

View file

@ -3,6 +3,8 @@ package handlers
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/tls"
"crypto/x509"
"expvar" "expvar"
"fmt" "fmt"
"math" "math"
@ -77,7 +79,7 @@ type App struct {
source notifications.SourceRecord source notifications.SourceRecord
} }
redis *redis.Client redis redis.UniversalClient
// isCache is true if this registry is configured as a pull through cache // isCache is true if this registry is configured as a pull through cache
isCache bool isCache bool
@ -114,7 +116,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
storageParams = make(configuration.Parameters) storageParams = make(configuration.Parameters)
} }
if storageParams["useragent"] == "" { if storageParams["useragent"] == "" {
storageParams["useragent"] = fmt.Sprintf("distribution/%s %s", version.Version, runtime.Version()) storageParams["useragent"] = fmt.Sprintf("distribution/%s %s", version.Version(), runtime.Version())
} }
var err error var err error
@ -155,7 +157,11 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
panic(err) panic(err)
} }
// Do not configure HTTP secret for a proxy registry as HTTP secret
// is only used for blob uploads and a proxy registry does not support blob uploads.
if !app.isCache {
app.configureSecret(config) app.configureSecret(config)
}
app.configureEvents(config) app.configureEvents(config)
app.configureRedis(config) app.configureRedis(config)
app.configureLogHook(config) app.configureLogHook(config)
@ -184,6 +190,21 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
} }
} }
// configure tag lookup concurrency limit
if p := config.Storage.TagParameters(); p != nil {
l, ok := p["concurrencylimit"]
if ok {
limit, ok := l.(int)
if !ok {
panic("tag lookup concurrency limit config key must have a integer value")
}
if limit < 0 {
panic("tag lookup concurrency limit should be a non-negative integer value")
}
options = append(options, storage.TagLookupConcurrencyLimit(limit))
}
}
// configure redirects // configure redirects
var redirectDisabled bool var redirectDisabled bool
if redirectConfig, ok := config.Storage["redirect"]; ok { if redirectConfig, ok := config.Storage["redirect"]; ok {
@ -236,6 +257,21 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
options = append(options, storage.ManifestURLsDenyRegexp(re)) options = append(options, storage.ManifestURLsDenyRegexp(re))
} }
} }
switch config.Validation.Manifests.Indexes.Platforms {
case "list":
options = append(options, storage.EnableValidateImageIndexImagesExist)
for _, platform := range config.Validation.Manifests.Indexes.PlatformList {
options = append(options, storage.AddValidateImageIndexImagesExistPlatform(platform.Architecture, platform.OS))
}
fallthrough
case "none":
dcontext.GetLogger(app).Warn("Image index completeness validation has been disabled, which is an experimental option because other container tooling might expect all image indexes to be complete")
case "all":
fallthrough
default:
options = append(options, storage.EnableValidateImageIndexImagesExist)
}
} }
// configure storage caches // configure storage caches
@ -411,6 +447,14 @@ func (app *App) RegisterHealthChecks(healthRegistries ...*health.Registry) {
} }
} }
// Shutdown close the underlying registry
func (app *App) Shutdown() error {
if r, ok := app.registry.(proxy.Closer); ok {
return r.Close()
}
return nil
}
// register a handler with the application, by route name. The handler will be // register a handler with the application, by route name. The handler will be
// passed through the application filters and context will be constructed at // passed through the application filters and context will be constructed at
// request time. // request time.
@ -487,12 +531,41 @@ func (app *App) configureEvents(configuration *configuration.Configuration) {
} }
func (app *App) configureRedis(cfg *configuration.Configuration) { func (app *App) configureRedis(cfg *configuration.Configuration) {
if cfg.Redis.Addr == "" { if len(cfg.Redis.Options.Addrs) == 0 {
dcontext.GetLogger(app).Infof("redis not configured") dcontext.GetLogger(app).Infof("redis not configured")
return return
} }
app.redis = app.createPool(cfg.Redis) // redis TLS config
if cfg.Redis.TLS.Certificate != "" || cfg.Redis.TLS.Key != "" {
var err error
tlsConf := &tls.Config{}
tlsConf.Certificates = make([]tls.Certificate, 1)
tlsConf.Certificates[0], err = tls.LoadX509KeyPair(cfg.Redis.TLS.Certificate, cfg.Redis.TLS.Key)
if err != nil {
panic(err)
}
if len(cfg.Redis.TLS.ClientCAs) != 0 {
pool := x509.NewCertPool()
for _, ca := range cfg.Redis.TLS.ClientCAs {
caPem, err := os.ReadFile(ca)
if err != nil {
dcontext.GetLogger(app).Errorf("failed reading redis client CA: %v", err)
return
}
if ok := pool.AppendCertsFromPEM(caPem); !ok {
dcontext.GetLogger(app).Error("could not add CA to pool")
return
}
}
tlsConf.ClientAuth = tls.RequireAndVerifyClientCert
tlsConf.ClientCAs = pool
}
cfg.Redis.Options.TLSConfig = tlsConf
}
app.redis = app.createPool(cfg.Redis.Options)
// Enable metrics instrumentation. // Enable metrics instrumentation.
if err := redisotel.InstrumentMetrics(app.redis); err != nil { if err := redisotel.InstrumentMetrics(app.redis); err != nil {
@ -514,25 +587,12 @@ func (app *App) configureRedis(cfg *configuration.Configuration) {
})) }))
} }
func (app *App) createPool(cfg configuration.Redis) *redis.Client { func (app *App) createPool(cfg redis.UniversalOptions) redis.UniversalClient {
return redis.NewClient(&redis.Options{ cfg.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
Addr: cfg.Addr,
OnConnect: func(ctx context.Context, cn *redis.Conn) error {
res := cn.Ping(ctx) res := cn.Ping(ctx)
return res.Err() return res.Err()
}, }
Username: cfg.Username, return redis.NewUniversalClient(&cfg)
Password: cfg.Password,
DB: cfg.DB,
MaxRetries: 3,
DialTimeout: cfg.DialTimeout,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
PoolFIFO: false,
MaxIdleConns: cfg.Pool.MaxIdle,
PoolSize: cfg.Pool.MaxActive,
ConnMaxIdleTime: cfg.Pool.IdleTimeout,
})
} }
// configureLogHook prepares logging hook parameters. // configureLogHook prepares logging hook parameters.

View file

@ -6,6 +6,7 @@ import (
"mime" "mime"
"net/http" "net/http"
"strings" "strings"
"sync"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/internal/dcontext" "github.com/distribution/distribution/v3/internal/dcontext"
@ -13,11 +14,13 @@ import (
"github.com/distribution/distribution/v3/manifest/ocischema" "github.com/distribution/distribution/v3/manifest/ocischema"
"github.com/distribution/distribution/v3/manifest/schema2" "github.com/distribution/distribution/v3/manifest/schema2"
"github.com/distribution/distribution/v3/registry/api/errcode" "github.com/distribution/distribution/v3/registry/api/errcode"
"github.com/distribution/distribution/v3/registry/storage"
"github.com/distribution/distribution/v3/registry/storage/driver" "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/reference" "github.com/distribution/reference"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/opencontainers/go-digest" "github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1" v1 "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
) )
const ( const (
@ -212,6 +215,11 @@ func (imh *manifestHandler) GetManifest(w http.ResponseWriter, r *http.Request)
w.Header().Set("Content-Length", fmt.Sprint(len(p))) w.Header().Set("Content-Length", fmt.Sprint(len(p)))
w.Header().Set("Docker-Content-Digest", imh.Digest.String()) w.Header().Set("Docker-Content-Digest", imh.Digest.String())
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
if r.Method == http.MethodHead {
return
}
if _, err := w.Write(p); err != nil { if _, err := w.Write(p); err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
} }
@ -476,12 +484,26 @@ func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Reques
return return
} }
var (
errs []error
mu sync.Mutex
)
g := errgroup.Group{}
g.SetLimit(storage.DefaultConcurrencyLimit)
for _, tag := range referencedTags { for _, tag := range referencedTags {
tag := tag
g.Go(func() error {
if err := tagService.Untag(imh, tag); err != nil { if err := tagService.Untag(imh, tag); err != nil {
imh.Errors = append(imh.Errors, err) mu.Lock()
return errs = append(errs, err)
mu.Unlock()
} }
return nil
})
} }
_ = g.Wait() // imh will record all errors, so ignore the error of Wait()
imh.Errors = errs
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
} }

View file

@ -17,14 +17,23 @@ type userpass struct {
password string password string
} }
func (u userpass) Basic(_ *url.URL) (string, string) {
return u.username, u.password
}
func (u userpass) RefreshToken(_ *url.URL, service string) string {
return ""
}
func (u userpass) SetRefreshToken(_ *url.URL, service, token string) {
}
type credentials struct { type credentials struct {
creds map[string]userpass creds map[string]userpass
} }
func (c credentials) Basic(u *url.URL) (string, string) { func (c credentials) Basic(u *url.URL) (string, string) {
up := c.creds[u.String()] return c.creds[u.String()].Basic(u)
return up.username, up.password
} }
func (c credentials) RefreshToken(u *url.URL, service string) string { func (c credentials) RefreshToken(u *url.URL, service string) string {
@ -35,12 +44,12 @@ func (c credentials) SetRefreshToken(u *url.URL, service, token string) {
} }
// configureAuth stores credentials for challenge responses // configureAuth stores credentials for challenge responses
func configureAuth(username, password, remoteURL string) (auth.CredentialStore, error) { func configureAuth(username, password, remoteURL string) (auth.CredentialStore, auth.CredentialStore, error) {
creds := map[string]userpass{} creds := map[string]userpass{}
authURLs, err := getAuthURLs(remoteURL) authURLs, err := getAuthURLs(remoteURL)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for _, url := range authURLs { for _, url := range authURLs {
@ -51,7 +60,7 @@ func configureAuth(username, password, remoteURL string) (auth.CredentialStore,
} }
} }
return credentials{creds: creds}, nil return credentials{creds: creds}, userpass{username: username, password: password}, nil
} }
func getAuthURLs(remoteURL string) ([]string, error) { func getAuthURLs(remoteURL string) ([]string, error) {

View file

@ -33,22 +33,20 @@ var inflight = make(map[digest.Digest]struct{})
// mu protects inflight // mu protects inflight
var mu sync.Mutex var mu sync.Mutex
func setResponseHeaders(w http.ResponseWriter, length int64, mediaType string, digest digest.Digest) { func setResponseHeaders(h http.Header, length int64, mediaType string, digest digest.Digest) {
w.Header().Set("Content-Length", strconv.FormatInt(length, 10)) h.Set("Content-Length", strconv.FormatInt(length, 10))
w.Header().Set("Content-Type", mediaType) h.Set("Content-Type", mediaType)
w.Header().Set("Docker-Content-Digest", digest.String()) h.Set("Docker-Content-Digest", digest.String())
w.Header().Set("Etag", digest.String()) h.Set("Etag", digest.String())
} }
func (pbs *proxyBlobStore) copyContent(ctx context.Context, dgst digest.Digest, writer io.Writer) (distribution.Descriptor, error) { func (pbs *proxyBlobStore) copyContent(ctx context.Context, dgst digest.Digest, writer io.Writer, h http.Header) (distribution.Descriptor, error) {
desc, err := pbs.remoteStore.Stat(ctx, dgst) desc, err := pbs.remoteStore.Stat(ctx, dgst)
if err != nil { if err != nil {
return distribution.Descriptor{}, err return distribution.Descriptor{}, err
} }
if w, ok := writer.(http.ResponseWriter); ok { setResponseHeaders(h, desc.Size, desc.MediaType, dgst)
setResponseHeaders(w, desc.Size, desc.MediaType, dgst)
}
remoteReader, err := pbs.remoteStore.Open(ctx, dgst) remoteReader, err := pbs.remoteStore.Open(ctx, dgst)
if err != nil { if err != nil {
@ -102,7 +100,7 @@ func (pbs *proxyBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter,
// Will return the blob from the remote store directly. // Will return the blob from the remote store directly.
// TODO Maybe we could reuse the these blobs are serving remotely and caching locally. // TODO Maybe we could reuse the these blobs are serving remotely and caching locally.
mu.Unlock() mu.Unlock()
_, err := pbs.copyContent(ctx, dgst, w) _, err := pbs.copyContent(ctx, dgst, w, w.Header())
return err return err
} }
inflight[dgst] = struct{}{} inflight[dgst] = struct{}{}
@ -122,7 +120,7 @@ func (pbs *proxyBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter,
// Serving client and storing locally over same fetching request. // Serving client and storing locally over same fetching request.
// This can prevent a redundant blob fetching. // This can prevent a redundant blob fetching.
multiWriter := io.MultiWriter(w, bw) multiWriter := io.MultiWriter(w, bw)
desc, err := pbs.copyContent(ctx, dgst, multiWriter) desc, err := pbs.copyContent(ctx, dgst, multiWriter, w.Header())
if err != nil { if err != nil {
return err return err
} }

View file

@ -448,12 +448,22 @@ func testProxyStoreServe(t *testing.T, te *testEnv, numClients int) {
return return
} }
bodyBytes := w.Body.Bytes() resp := w.Result()
bodyBytes, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
t.Errorf(err.Error())
return
}
localDigest := digest.FromBytes(bodyBytes) localDigest := digest.FromBytes(bodyBytes)
if localDigest != remoteBlob.Digest { if localDigest != remoteBlob.Digest {
t.Errorf("Mismatching blob fetch from proxy") t.Errorf("Mismatching blob fetch from proxy")
return return
} }
if resp.Header.Get("Docker-Content-Digest") != localDigest.String() {
t.Errorf("Mismatching digest in response header")
return
}
desc, err := te.store.localStore.Stat(te.ctx, remoteBlob.Digest) desc, err := te.store.localStore.Stat(te.ctx, remoteBlob.Digest)
if err != nil { if err != nil {

View file

@ -62,6 +62,16 @@ func init() {
})) }))
metrics.Register(prometheus.ProxyNamespace) metrics.Register(prometheus.ProxyNamespace)
initPrometheusMetrics("blob")
initPrometheusMetrics("manifest")
}
func initPrometheusMetrics(value string) {
requests.WithValues(value).Inc(0)
hits.WithValues(value).Inc(0)
misses.WithValues(value).Inc(0)
pulledBytes.WithValues(value).Inc(0)
pushedBytes.WithValues(value).Inc(0)
} }
// BlobPull tracks metrics about blobs pulled into the cache // BlobPull tracks metrics about blobs pulled into the cache

View file

@ -8,6 +8,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/distribution/reference"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/configuration"
"github.com/distribution/distribution/v3/internal/client" "github.com/distribution/distribution/v3/internal/client"
@ -18,7 +20,6 @@ import (
"github.com/distribution/distribution/v3/registry/proxy/scheduler" "github.com/distribution/distribution/v3/registry/proxy/scheduler"
"github.com/distribution/distribution/v3/registry/storage" "github.com/distribution/distribution/v3/registry/storage"
"github.com/distribution/distribution/v3/registry/storage/driver" "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/reference"
) )
var repositoryTTL = 24 * 7 * time.Hour var repositoryTTL = 24 * 7 * time.Hour
@ -30,6 +31,7 @@ type proxyingRegistry struct {
ttl *time.Duration ttl *time.Duration
remoteURL url.URL remoteURL url.URL
authChallenger authChallenger authChallenger authChallenger
basicAuth auth.CredentialStore
} }
// NewRegistryPullThroughCache creates a registry acting as a pull through cache // NewRegistryPullThroughCache creates a registry acting as a pull through cache
@ -112,7 +114,7 @@ func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Name
} }
} }
cs, err := configureAuth(config.Username, config.Password, config.RemoteURL) cs, b, err := configureAuth(config.Username, config.Password, config.RemoteURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -127,6 +129,7 @@ func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Name
cm: challenge.NewSimpleManager(), cm: challenge.NewSimpleManager(),
cs: cs, cs: cs,
}, },
basicAuth: b,
}, nil }, nil
} }
@ -155,7 +158,8 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name reference.Named
tr := transport.NewTransport(http.DefaultTransport, tr := transport.NewTransport(http.DefaultTransport,
auth.NewAuthorizer(c.challengeManager(), auth.NewAuthorizer(c.challengeManager(),
auth.NewTokenHandlerWithOptions(tkopts))) auth.NewTokenHandlerWithOptions(tkopts),
auth.NewBasicHandler(pr.basicAuth)))
localRepo, err := pr.embedded.Repository(ctx, name) localRepo, err := pr.embedded.Repository(ctx, name)
if err != nil { if err != nil {
@ -211,6 +215,15 @@ func (pr *proxyingRegistry) BlobStatter() distribution.BlobStatter {
return pr.embedded.BlobStatter() return pr.embedded.BlobStatter()
} }
type Closer interface {
// Close release all resources used by this object
Close() error
}
func (pr *proxyingRegistry) Close() error {
return pr.scheduler.Stop()
}
// authChallenger encapsulates a request to the upstream to establish credential challenges // authChallenger encapsulates a request to the upstream to establish credential challenges
type authChallenger interface { type authChallenger interface {
tryEstablishChallenges(context.Context) error tryEstablishChallenges(context.Context) error

View file

@ -206,12 +206,13 @@ func (ttles *TTLExpirationScheduler) startTimer(entry *schedulerEntry, ttl time.
} }
// Stop stops the scheduler. // Stop stops the scheduler.
func (ttles *TTLExpirationScheduler) Stop() { func (ttles *TTLExpirationScheduler) Stop() error {
ttles.Lock() ttles.Lock()
defer ttles.Unlock() defer ttles.Unlock()
if err := ttles.writeState(); err != nil { err := ttles.writeState()
dcontext.GetLogger(ttles.ctx).Errorf("Error writing scheduler state: %s", err) if err != nil {
err = fmt.Errorf("error writing scheduler state: %w", err)
} }
for _, entry := range ttles.entries { for _, entry := range ttles.entries {
@ -221,6 +222,7 @@ func (ttles *TTLExpirationScheduler) Stop() {
close(ttles.doneChan) close(ttles.doneChan)
ttles.saveTimer.Stop() ttles.saveTimer.Stop()
ttles.stopped = true ttles.stopped = true
return err
} }
func (ttles *TTLExpirationScheduler) writeState() error { func (ttles *TTLExpirationScheduler) writeState() error {

View file

@ -136,7 +136,12 @@ func TestRestoreOld(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Error starting ttlExpirationScheduler: %s", err) t.Fatalf("Error starting ttlExpirationScheduler: %s", err)
} }
defer s.Stop() defer func(s *TTLExpirationScheduler) {
err := s.Stop()
if err != nil {
t.Fatalf("Error stopping ttlExpirationScheduler: %s", err)
}
}(s)
wg.Wait() wg.Wait()
mu.Lock() mu.Lock()
@ -177,7 +182,10 @@ func TestStopRestore(t *testing.T) {
// Start and stop before all operations complete // Start and stop before all operations complete
// state will be written to fs // state will be written to fs
s.Stop() err = s.Stop()
if err != nil {
t.Fatalf(err.Error())
}
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
// v2 will restore state from fs // v2 will restore state from fs

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -20,6 +21,8 @@ import (
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"golang.org/x/crypto/acme" "golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/distribution/distribution/v3/configuration" "github.com/distribution/distribution/v3/configuration"
"github.com/distribution/distribution/v3/health" "github.com/distribution/distribution/v3/health"
@ -79,9 +82,6 @@ var tlsVersions = map[string]uint16{
// defaultLogFormatter is the default formatter to use for logs. // defaultLogFormatter is the default formatter to use for logs.
const defaultLogFormatter = "text" const defaultLogFormatter = "text"
// this channel gets notified when process receives signal. It is global to ease unit testing
var quit = make(chan os.Signal, 1)
// HandlerFunc defines an http middleware // HandlerFunc defines an http middleware
type HandlerFunc func(config *configuration.Configuration, handler http.Handler) http.Handler type HandlerFunc func(config *configuration.Configuration, handler http.Handler) http.Handler
@ -99,7 +99,7 @@ var ServeCmd = &cobra.Command{
Long: "`serve` stores and distributes Docker images.", Long: "`serve` stores and distributes Docker images.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// setup context // setup context
ctx := dcontext.WithVersion(dcontext.Background(), version.Version) ctx := dcontext.WithVersion(dcontext.Background(), version.Version())
config, err := resolveConfiguration(args) config, err := resolveConfiguration(args)
if err != nil { if err != nil {
@ -128,6 +128,7 @@ type Registry struct {
config *configuration.Configuration config *configuration.Configuration
app *handlers.App app *handlers.App
server *http.Server server *http.Server
quit chan os.Signal
} }
// NewRegistry creates a new registry from a context and configuration struct. // NewRegistry creates a new registry from a context and configuration struct.
@ -158,6 +159,9 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg
if err != nil { if err != nil {
return nil, fmt.Errorf("error during open telemetry initialization: %v", err) return nil, fmt.Errorf("error during open telemetry initialization: %v", err)
} }
if config.HTTP.H2C.Enabled {
handler = h2c.NewHandler(handler, &http2.Server{})
}
handler = otelHandler(handler) handler = otelHandler(handler)
server := &http.Server{ server := &http.Server{
@ -168,6 +172,7 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg
app: app, app: app,
config: config, config: config,
server: server, server: server,
quit: make(chan os.Signal, 1),
}, nil }, nil
} }
@ -308,7 +313,7 @@ func (registry *Registry) ListenAndServe() error {
} }
// setup channel to get notified on SIGTERM signal // setup channel to get notified on SIGTERM signal
signal.Notify(quit, syscall.SIGTERM) signal.Notify(registry.quit, os.Interrupt, syscall.SIGTERM)
serveErr := make(chan error) serveErr := make(chan error)
// Start serving in goroutine and listen for stop signal in main thread // Start serving in goroutine and listen for stop signal in main thread
@ -319,15 +324,24 @@ func (registry *Registry) ListenAndServe() error {
select { select {
case err := <-serveErr: case err := <-serveErr:
return err return err
case <-quit: case <-registry.quit:
dcontext.GetLogger(registry.app).Info("stopping server gracefully. Draining connections for ", config.HTTP.DrainTimeout) dcontext.GetLogger(registry.app).Info("stopping server gracefully. Draining connections for ", config.HTTP.DrainTimeout)
// shutdown the server with a grace period of configured timeout // shutdown the server with a grace period of configured timeout
c, cancel := context.WithTimeout(context.Background(), config.HTTP.DrainTimeout) c, cancel := context.WithTimeout(context.Background(), config.HTTP.DrainTimeout)
defer cancel() defer cancel()
return registry.server.Shutdown(c) return registry.Shutdown(c)
} }
} }
// Shutdown gracefully shuts down the registry's HTTP server and application object.
func (registry *Registry) Shutdown(ctx context.Context) error {
err := registry.server.Shutdown(ctx)
if appErr := registry.app.Shutdown(); appErr != nil {
err = errors.Join(err, appErr)
}
return err
}
func configureDebugServer(config *configuration.Configuration) { func configureDebugServer(config *configuration.Configuration) {
if config.HTTP.Debug.Addr != "" { if config.HTTP.Debug.Addr != "" {
go func(addr string) { go func(addr string) {

View file

@ -103,7 +103,7 @@ func TestGracefulShutdown(t *testing.T) {
fmt.Fprintf(conn, "GET /v2/ ") fmt.Fprintf(conn, "GET /v2/ ")
// send stop signal // send stop signal
quit <- os.Interrupt registry.quit <- os.Interrupt
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
// try connecting again. it shouldn't // try connecting again. it shouldn't
@ -325,7 +325,7 @@ func TestRegistrySupportedCipherSuite(t *testing.T) {
} }
// send stop signal // send stop signal
quit <- os.Interrupt registry.quit <- os.Interrupt
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
@ -369,7 +369,7 @@ func TestRegistryUnsupportedCipherSuite(t *testing.T) {
} }
// send stop signal // send stop signal
quit <- os.Interrupt registry.quit <- os.Interrupt
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }

View file

@ -40,7 +40,7 @@ func TestWriteSeek(t *testing.T) {
} }
contents := []byte{1, 2, 3} contents := []byte{1, 2, 3}
if _, err := blobUpload.Write(contents); err != nil { if _, err := blobUpload.Write(contents); err != nil {
t.Fatalf("unexpected error writing contets: %v", err) t.Fatalf("unexpected error writing contents: %v", err)
} }
blobUpload.Close() blobUpload.Close()
offset := blobUpload.Size() offset := blobUpload.Size()

View file

@ -230,7 +230,7 @@ func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descri
} }
if fullHash { if fullHash {
// a fantastic optimization: if the the written data and the size are // a fantastic optimization: if the written data and the size are
// the same, we don't need to read the data from the backend. This is // the same, we don't need to read the data from the backend. This is
// because we've written the entire file in the lifecycle of the // because we've written the entire file in the lifecycle of the
// current instance. // current instance.

View file

@ -25,7 +25,7 @@ import (
// Note that there is no implied relationship between these two caches. The // Note that there is no implied relationship between these two caches. The
// layer may exist in one, both or none and the code must be written this way. // layer may exist in one, both or none and the code must be written this way.
type redisBlobDescriptorService struct { type redisBlobDescriptorService struct {
pool *redis.Client pool redis.UniversalClient
// TODO(stevvooe): We use a pool because we don't have great control over // TODO(stevvooe): We use a pool because we don't have great control over
// the cache lifecycle to manage connections. A new connection if fetched // the cache lifecycle to manage connections. A new connection if fetched
@ -37,7 +37,7 @@ var _ distribution.BlobDescriptorService = &redisBlobDescriptorService{}
// NewRedisBlobDescriptorCacheProvider returns a new redis-based // NewRedisBlobDescriptorCacheProvider returns a new redis-based
// BlobDescriptorCacheProvider using the provided redis connection pool. // BlobDescriptorCacheProvider using the provided redis connection pool.
func NewRedisBlobDescriptorCacheProvider(pool *redis.Client) cache.BlobDescriptorCacheProvider { func NewRedisBlobDescriptorCacheProvider(pool redis.UniversalClient) cache.BlobDescriptorCacheProvider {
return metrics.NewPrometheusCacheProvider( return metrics.NewPrometheusCacheProvider(
&redisBlobDescriptorService{ &redisBlobDescriptorService{
pool: pool, pool: pool,

View file

@ -20,7 +20,7 @@ func init() {
// implementation. // implementation.
func TestRedisBlobDescriptorCacheProvider(t *testing.T) { func TestRedisBlobDescriptorCacheProvider(t *testing.T) {
if redisAddr == "" { if redisAddr == "" {
// fallback to an environement variable // fallback to an environment variable
redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR") redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR")
} }

View file

@ -46,12 +46,20 @@ import (
"github.com/distribution/distribution/v3/internal/dcontext" "github.com/distribution/distribution/v3/internal/dcontext"
prometheus "github.com/distribution/distribution/v3/metrics" prometheus "github.com/distribution/distribution/v3/metrics"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver" storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/tracing"
"github.com/docker/go-metrics" "github.com/docker/go-metrics"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
) )
// storageAction is the metrics of blob related operations // storageAction is the metrics of blob related operations
var storageAction = prometheus.StorageNamespace.NewLabeledTimer("action", "The number of seconds that the storage action takes", "driver", "action") var storageAction = prometheus.StorageNamespace.NewLabeledTimer("action", "The number of seconds that the storage action takes", "driver", "action")
// tracer is the OpenTelemetry tracer utilized for tracing operations within
// this package's code.
var tracer = otel.Tracer("github.com/distribution/distribution/v3/registry/storage/driver/base")
func init() { func init() {
metrics.Register(prometheus.StorageNamespace) metrics.Register(prometheus.StorageNamespace)
} }
@ -89,8 +97,16 @@ func (base *Base) setDriverName(e error) error {
// GetContent wraps GetContent of underlying storage driver. // GetContent wraps GetContent of underlying storage driver.
func (base *Base) GetContent(ctx context.Context, path string) ([]byte, error) { func (base *Base) GetContent(ctx context.Context, path string) ([]byte, error) {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.GetContent(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
ctx,
"GetContent",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) { if !storagedriver.PathRegexp.MatchString(path) {
return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -104,8 +120,17 @@ func (base *Base) GetContent(ctx context.Context, path string) ([]byte, error) {
// PutContent wraps PutContent of underlying storage driver. // PutContent wraps PutContent of underlying storage driver.
func (base *Base) PutContent(ctx context.Context, path string, content []byte) error { func (base *Base) PutContent(ctx context.Context, path string, content []byte) error {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.PutContent(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
attribute.Int(tracing.AttributePrefix+"storage.content.length", len(content)),
}
ctx, span := tracer.Start(
ctx,
"PutContent",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) { if !storagedriver.PathRegexp.MatchString(path) {
return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -119,8 +144,17 @@ func (base *Base) PutContent(ctx context.Context, path string, content []byte) e
// Reader wraps Reader of underlying storage driver. // Reader wraps Reader of underlying storage driver.
func (base *Base) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { func (base *Base) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.Reader(%q, %d)", base.Name(), path, offset) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
attribute.Int64(tracing.AttributePrefix+"storage.offset", offset),
}
ctx, span := tracer.Start(
ctx,
"Reader",
trace.WithAttributes(attrs...))
defer span.End()
if offset < 0 { if offset < 0 {
return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset, DriverName: base.StorageDriver.Name()} return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset, DriverName: base.StorageDriver.Name()}
@ -136,8 +170,17 @@ func (base *Base) Reader(ctx context.Context, path string, offset int64) (io.Rea
// Writer wraps Writer of underlying storage driver. // Writer wraps Writer of underlying storage driver.
func (base *Base) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { func (base *Base) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.Writer(%q, %v)", base.Name(), path, append) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
attribute.Bool(tracing.AttributePrefix+"storage.append", append),
}
ctx, span := tracer.Start(
ctx,
"Writer",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) { if !storagedriver.PathRegexp.MatchString(path) {
return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -149,8 +192,16 @@ func (base *Base) Writer(ctx context.Context, path string, append bool) (storage
// Stat wraps Stat of underlying storage driver. // Stat wraps Stat of underlying storage driver.
func (base *Base) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { func (base *Base) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.Stat(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
ctx,
"Stat",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) && path != "/" { if !storagedriver.PathRegexp.MatchString(path) && path != "/" {
return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -164,8 +215,16 @@ func (base *Base) Stat(ctx context.Context, path string) (storagedriver.FileInfo
// List wraps List of underlying storage driver. // List wraps List of underlying storage driver.
func (base *Base) List(ctx context.Context, path string) ([]string, error) { func (base *Base) List(ctx context.Context, path string) ([]string, error) {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.List(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
ctx,
"List",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) && path != "/" { if !storagedriver.PathRegexp.MatchString(path) && path != "/" {
return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -179,6 +238,18 @@ func (base *Base) List(ctx context.Context, path string) ([]string, error) {
// Move wraps Move of underlying storage driver. // Move wraps Move of underlying storage driver.
func (base *Base) Move(ctx context.Context, sourcePath string, destPath string) error { func (base *Base) Move(ctx context.Context, sourcePath string, destPath string) error {
attrs := []attribute.KeyValue{
attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.source.path", sourcePath),
attribute.String(tracing.AttributePrefix+"storage.dest.path", destPath),
}
ctx, span := tracer.Start(
ctx,
"Move",
trace.WithAttributes(attrs...))
defer span.End()
ctx, done := dcontext.WithTrace(ctx) ctx, done := dcontext.WithTrace(ctx)
defer done("%s.Move(%q, %q", base.Name(), sourcePath, destPath) defer done("%s.Move(%q, %q", base.Name(), sourcePath, destPath)
@ -196,8 +267,16 @@ func (base *Base) Move(ctx context.Context, sourcePath string, destPath string)
// Delete wraps Delete of underlying storage driver. // Delete wraps Delete of underlying storage driver.
func (base *Base) Delete(ctx context.Context, path string) error { func (base *Base) Delete(ctx context.Context, path string) error {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.Delete(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
ctx,
"Delete",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) { if !storagedriver.PathRegexp.MatchString(path) {
return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -211,8 +290,16 @@ func (base *Base) Delete(ctx context.Context, path string) error {
// RedirectURL wraps RedirectURL of the underlying storage driver. // RedirectURL wraps RedirectURL of the underlying storage driver.
func (base *Base) RedirectURL(r *http.Request, path string) (string, error) { func (base *Base) RedirectURL(r *http.Request, path string) (string, error) {
ctx, done := dcontext.WithTrace(r.Context()) attrs := []attribute.KeyValue{
defer done("%s.RedirectURL(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
r.Context(),
"RedirectURL",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) { if !storagedriver.PathRegexp.MatchString(path) {
return "", storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return "", storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
@ -226,8 +313,16 @@ func (base *Base) RedirectURL(r *http.Request, path string) (string, error) {
// Walk wraps Walk of underlying storage driver. // Walk wraps Walk of underlying storage driver.
func (base *Base) Walk(ctx context.Context, path string, f storagedriver.WalkFn, options ...func(*storagedriver.WalkOptions)) error { func (base *Base) Walk(ctx context.Context, path string, f storagedriver.WalkFn, options ...func(*storagedriver.WalkOptions)) error {
ctx, done := dcontext.WithTrace(ctx) attrs := []attribute.KeyValue{
defer done("%s.Walk(%q)", base.Name(), path) attribute.String(tracing.AttributePrefix+"storage.driver.name", base.Name()),
attribute.String(tracing.AttributePrefix+"storage.path", path),
}
ctx, span := tracer.Start(
ctx,
"Walk",
trace.WithAttributes(attrs...))
defer span.End()
if !storagedriver.PathRegexp.MatchString(path) && path != "/" { if !storagedriver.PathRegexp.MatchString(path) && path != "/" {
return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}

View file

@ -155,6 +155,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
jwtConf := new(jwt.Config) jwtConf := new(jwt.Config)
var err error var err error
var gcs *storage.Client var gcs *storage.Client
var options []option.ClientOption
if keyfile, ok := parameters["keyfile"]; ok { if keyfile, ok := parameters["keyfile"]; ok {
jsonKey, err := os.ReadFile(fmt.Sprint(keyfile)) jsonKey, err := os.ReadFile(fmt.Sprint(keyfile))
if err != nil { if err != nil {
@ -165,10 +166,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
return nil, err return nil, err
} }
ts = jwtConf.TokenSource(ctx) ts = jwtConf.TokenSource(ctx)
gcs, err = storage.NewClient(ctx, option.WithCredentialsFile(fmt.Sprint(keyfile))) options = append(options, option.WithCredentialsFile(fmt.Sprint(keyfile)))
if err != nil {
return nil, err
}
} else if credentials, ok := parameters["credentials"]; ok { } else if credentials, ok := parameters["credentials"]; ok {
credentialMap, ok := credentials.(map[interface{}]interface{}) credentialMap, ok := credentials.(map[interface{}]interface{})
if !ok { if !ok {
@ -194,10 +192,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
return nil, err return nil, err
} }
ts = jwtConf.TokenSource(ctx) ts = jwtConf.TokenSource(ctx)
gcs, err = storage.NewClient(ctx, option.WithCredentialsJSON(data)) options = append(options, option.WithCredentialsJSON(data))
if err != nil {
return nil, err
}
} else { } else {
var err error var err error
// DefaultTokenSource is a convenience method. It first calls FindDefaultCredentials, // DefaultTokenSource is a convenience method. It first calls FindDefaultCredentials,
@ -207,11 +202,18 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
if err != nil { if err != nil {
return nil, err return nil, err
} }
gcs, err = storage.NewClient(ctx) }
if userAgent, ok := parameters["useragent"]; ok {
if ua, ok := userAgent.(string); ok && ua != "" {
options = append(options, option.WithUserAgent(ua))
}
}
gcs, err = storage.NewClient(ctx, options...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
}
maxConcurrency, err := base.GetLimitFromParameter(parameters["maxconcurrency"], minConcurrency, defaultMaxConcurrency) maxConcurrency, err := base.GetLimitFromParameter(parameters["maxconcurrency"], minConcurrency, defaultMaxConcurrency)
if err != nil { if err != nil {
@ -783,10 +785,6 @@ func (d *driver) Delete(ctx context.Context, path string) error {
// RedirectURL returns a URL which may be used to retrieve the content stored at // RedirectURL returns a URL which may be used to retrieve the content stored at
// the given path, possibly using the given options. // the given path, possibly using the given options.
func (d *driver) RedirectURL(r *http.Request, path string) (string, error) { func (d *driver) RedirectURL(r *http.Request, path string) (string, error) {
if d.privateKey == nil {
return "", nil
}
if r.Method != http.MethodGet && r.Method != http.MethodHead { if r.Method != http.MethodGet && r.Method != http.MethodHead {
return "", nil return "", nil
} }

View file

@ -34,6 +34,7 @@ func init() {
} }
} }
gcsDriverConstructor = func(rootDirectory string) (storagedriver.StorageDriver, error) {
jsonKey, err := os.ReadFile(credentials) jsonKey, err := os.ReadFile(credentials)
if err != nil { if err != nil {
panic(fmt.Sprintf("Error reading JSON key : %v", err)) panic(fmt.Sprintf("Error reading JSON key : %v", err))
@ -67,7 +68,6 @@ func init() {
panic(fmt.Sprintf("Error initializing gcs client : %v", err)) panic(fmt.Sprintf("Error initializing gcs client : %v", err))
} }
gcsDriverConstructor = func(rootDirectory string) (storagedriver.StorageDriver, error) {
parameters := driverParameters{ parameters := driverParameters{
bucket: bucket, bucket: bucket,
rootDirectory: rootDirectory, rootDirectory: rootDirectory,

View file

@ -50,6 +50,6 @@ pZeMRablbPQdp8/1NyIwimq1VlG0ohQ4P6qhW7E09ZMC
t.Fatal(err) t.Fatal(err)
} }
if storageDriver == nil { if storageDriver == nil {
t.Fatal("Driver couldnt be initialized.") t.Fatal("Driver could not be initialized")
} }
} }

View file

@ -0,0 +1,86 @@
package middleware
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
storagemiddleware "github.com/distribution/distribution/v3/registry/storage/driver/middleware"
"github.com/sirupsen/logrus"
)
func init() {
if err := storagemiddleware.Register("rewrite", newRewriteStorageMiddleware); err != nil {
logrus.Errorf("tailed to register redirect storage middleware: %v", err)
}
}
type rewriteStorageMiddleware struct {
storagedriver.StorageDriver
overrideScheme string
overrideHost string
trimPathPrefix string
}
var _ storagedriver.StorageDriver = &rewriteStorageMiddleware{}
func getStringOption(key string, options map[string]interface{}) (string, error) {
o, ok := options[key]
if !ok {
return "", nil
}
s, ok := o.(string)
if !ok {
return "", fmt.Errorf("%s must be a string", key)
}
return s, nil
}
func newRewriteStorageMiddleware(ctx context.Context, sd storagedriver.StorageDriver, options map[string]interface{}) (storagedriver.StorageDriver, error) {
var err error
r := &rewriteStorageMiddleware{StorageDriver: sd}
if r.overrideScheme, err = getStringOption("scheme", options); err != nil {
return nil, err
}
if r.overrideHost, err = getStringOption("host", options); err != nil {
return nil, err
}
if r.trimPathPrefix, err = getStringOption("trimpathprefix", options); err != nil {
return nil, err
}
return r, nil
}
func (r *rewriteStorageMiddleware) RedirectURL(req *http.Request, path string) (string, error) {
storagePath, err := r.StorageDriver.RedirectURL(req, path)
if err != nil {
return "", err
}
u, err := url.Parse(storagePath)
if err != nil {
return "", err
}
if r.overrideScheme != "" {
u.Scheme = r.overrideScheme
}
if r.overrideHost != "" {
u.Host = r.overrideHost
}
if r.trimPathPrefix != "" {
u.Path = strings.TrimPrefix(u.Path, r.trimPathPrefix)
}
return u.String(), nil
}

View file

@ -0,0 +1,75 @@
package middleware
import (
"context"
"net/http"
"testing"
"github.com/distribution/distribution/v3/registry/storage/driver/base"
"github.com/stretchr/testify/require"
)
type mockSD struct {
base.Base
}
func (*mockSD) RedirectURL(_ *http.Request, urlPath string) (string, error) {
return "http://some.host/some/path/file", nil
}
func TestNoConfig(t *testing.T) {
options := make(map[string]interface{})
middleware, err := newRewriteStorageMiddleware(context.Background(), &mockSD{}, options)
require.NoError(t, err)
_, ok := middleware.(*rewriteStorageMiddleware)
require.True(t, ok)
url, err := middleware.RedirectURL(nil, "")
require.NoError(t, err)
require.Equal(t, "http://some.host/some/path/file", url)
}
func TestWrongType(t *testing.T) {
options := map[string]interface{}{
"scheme": 1,
}
_, err := newRewriteStorageMiddleware(context.TODO(), nil, options)
require.ErrorContains(t, err, "scheme must be a string")
}
func TestRewriteHostsScheme(t *testing.T) {
options := map[string]interface{}{
"scheme": "https",
"host": "example.com",
}
middleware, err := newRewriteStorageMiddleware(context.TODO(), &mockSD{}, options)
require.NoError(t, err)
m, ok := middleware.(*rewriteStorageMiddleware)
require.True(t, ok)
require.Equal(t, "https", m.overrideScheme)
require.Equal(t, "example.com", m.overrideHost)
url, err := middleware.RedirectURL(nil, "")
require.NoError(t, err)
require.Equal(t, "https://example.com/some/path/file", url)
}
func TestTrimPrefix(t *testing.T) {
options := map[string]interface{}{
"trimpathprefix": "/some/path",
}
middleware, err := newRewriteStorageMiddleware(context.TODO(), &mockSD{}, options)
require.NoError(t, err)
m, ok := middleware.(*rewriteStorageMiddleware)
require.True(t, ok)
require.Equal(t, "/some/path", m.trimPathPrefix)
url, err := middleware.RedirectURL(nil, "")
require.NoError(t, err)
require.Equal(t, "http://some.host/file", url)
}

View file

@ -205,7 +205,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (*Dr
regionEndpoint = "" regionEndpoint = ""
} }
forcePathStyleBool := true forcePathStyleBool := false
forcePathStyle := parameters["forcepathstyle"] forcePathStyle := parameters["forcepathstyle"]
switch forcePathStyle := forcePathStyle.(type) { switch forcePathStyle := forcePathStyle.(type) {
case string: case string:
@ -529,9 +529,9 @@ func New(ctx context.Context, params DriverParameters) (*Driver, error) {
if params.RegionEndpoint != "" { if params.RegionEndpoint != "" {
awsConfig.WithEndpoint(params.RegionEndpoint) awsConfig.WithEndpoint(params.RegionEndpoint)
awsConfig.WithS3ForcePathStyle(params.ForcePathStyle)
} }
awsConfig.WithS3ForcePathStyle(params.ForcePathStyle)
awsConfig.WithS3UseAccelerate(params.Accelerate) awsConfig.WithS3UseAccelerate(params.Accelerate)
awsConfig.WithRegion(params.Region) awsConfig.WithRegion(params.Region)
awsConfig.WithDisableSSL(!params.Secure) awsConfig.WithDisableSSL(!params.Secure)
@ -1128,7 +1128,6 @@ func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, from stri
// get a list of all inferred directories between the previous directory and this file // get a list of all inferred directories between the previous directory and this file
dirs := directoryDiff(prevDir, filePath) dirs := directoryDiff(prevDir, filePath)
if len(dirs) > 0 {
for _, dir := range dirs { for _, dir := range dirs {
walkInfos = append(walkInfos, storagedriver.FileInfoInternal{ walkInfos = append(walkInfos, storagedriver.FileInfoInternal{
FileInfoFields: storagedriver.FileInfoFields{ FileInfoFields: storagedriver.FileInfoFields{
@ -1138,7 +1137,6 @@ func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, from stri
}) })
prevDir = dir prevDir = dir
} }
}
walkInfos = append(walkInfos, storagedriver.FileInfoInternal{ walkInfos = append(walkInfos, storagedriver.FileInfoInternal{
FileInfoFields: storagedriver.FileInfoFields{ FileInfoFields: storagedriver.FileInfoFields{

View file

@ -80,7 +80,7 @@ func init() {
return nil, err return nil, err
} }
} }
forcePathStyleBool := true forcePathStyleBool := false
if forcePathStyle != "" { if forcePathStyle != "" {
forcePathStyleBool, err = strconv.ParseBool(forcePathStyle) forcePathStyleBool, err = strconv.ParseBool(forcePathStyle)
if err != nil { if err != nil {
@ -203,6 +203,8 @@ func TestEmptyRootList(t *testing.T) {
} }
func TestClientTransport(t *testing.T) { func TestClientTransport(t *testing.T) {
skipCheck(t)
testCases := []struct { testCases := []struct {
skipverify bool skipverify bool
}{ }{
@ -243,7 +245,7 @@ func TestClientTransport(t *testing.T) {
return return
} }
// if tc.skipverify is false we do not override the driver // if tc.skipverify is false we do not override the driver
// HTTP clien transport and leave it to the AWS SDK. // HTTP client transport and leave it to the AWS SDK.
if s3drv.S3.Client.Config.HTTPClient.Transport != nil { if s3drv.S3.Client.Config.HTTPClient.Transport != nil {
t.Errorf("unexpected S3 driver client transport") t.Errorf("unexpected S3 driver client transport")
} }

View file

@ -2,6 +2,7 @@ package storage
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
@ -36,6 +37,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
// mark // mark
markSet := make(map[digest.Digest]struct{}) markSet := make(map[digest.Digest]struct{})
deleteLayerSet := make(map[string][]digest.Digest)
manifestArr := make([]ManifestDel, 0) manifestArr := make([]ManifestDel, 0)
err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
emit(repoName) emit(repoName)
@ -68,12 +70,15 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
return fmt.Errorf("failed to retrieve tags for digest %v: %v", dgst, err) return fmt.Errorf("failed to retrieve tags for digest %v: %v", dgst, err)
} }
if len(tags) == 0 { if len(tags) == 0 {
emit("manifest eligible for deletion: %s", dgst)
// fetch all tags from repository // fetch all tags from repository
// all of these tags could contain manifest in history // all of these tags could contain manifest in history
// which means that we need check (and delete) those references when deleting manifest // which means that we need check (and delete) those references when deleting manifest
allTags, err := repository.Tags(ctx).All(ctx) allTags, err := repository.Tags(ctx).All(ctx)
if err != nil { if err != nil {
if _, ok := err.(distribution.ErrRepositoryUnknown); ok {
emit("manifest tags path of repository %s does not exist", repoName)
return nil
}
return fmt.Errorf("failed to retrieve tags %v", err) return fmt.Errorf("failed to retrieve tags %v", err)
} }
manifestArr = append(manifestArr, ManifestDel{Name: repoName, Digest: dgst, Tags: allTags}) manifestArr = append(manifestArr, ManifestDel{Name: repoName, Digest: dgst, Tags: allTags})
@ -84,35 +89,50 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
emit("%s: marking manifest %s ", repoName, dgst) emit("%s: marking manifest %s ", repoName, dgst)
markSet[dgst] = struct{}{} markSet[dgst] = struct{}{}
manifest, err := manifestService.Get(ctx, dgst) return markManifestReferences(dgst, manifestService, ctx, func(d digest.Digest) bool {
if err != nil { _, marked := markSet[d]
return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err) if !marked {
markSet[d] = struct{}{}
emit("%s: marking blob %s", repoName, d)
} }
return marked
descriptors := manifest.References() })
for _, descriptor := range descriptors {
markSet[descriptor.Digest] = struct{}{}
emit("%s: marking blob %s", repoName, descriptor.Digest)
}
return nil
}) })
if err != nil {
// In certain situations such as unfinished uploads, deleting all // In certain situations such as unfinished uploads, deleting all
// tags in S3 or removing the _manifests folder manually, this // tags in S3 or removing the _manifests folder manually, this
// error may be of type PathNotFound. // error may be of type PathNotFound.
// //
// In these cases we can continue marking other manifests safely. // In these cases we can continue marking other manifests safely.
if _, ok := err.(driver.PathNotFoundError); ok { if _, ok := err.(driver.PathNotFoundError); !ok {
return nil return err
}
}
blobService := repository.Blobs(ctx)
layerEnumerator, ok := blobService.(distribution.ManifestEnumerator)
if !ok {
return errors.New("unable to convert BlobService into ManifestEnumerator")
} }
var deleteLayers []digest.Digest
err = layerEnumerator.Enumerate(ctx, func(dgst digest.Digest) error {
if _, ok := markSet[dgst]; !ok {
deleteLayers = append(deleteLayers, dgst)
}
return nil
})
if len(deleteLayers) > 0 {
deleteLayerSet[repoName] = deleteLayers
}
return err return err
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to mark: %v", err) return fmt.Errorf("failed to mark: %v", err)
} }
manifestArr = unmarkReferencedManifest(manifestArr, markSet)
// sweep // sweep
vacuum := NewVacuum(ctx, storageDriver) vacuum := NewVacuum(ctx, storageDriver)
if !opts.DryRun { if !opts.DryRun {
@ -147,5 +167,51 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
} }
} }
for repo, dgsts := range deleteLayerSet {
for _, dgst := range dgsts {
err = vacuum.RemoveLayer(repo, dgst)
if err != nil {
return fmt.Errorf("failed to delete layer link %s of repo %s: %v", dgst, repo, err)
}
}
}
return err return err
} }
// unmarkReferencedManifest filters out manifest present in markSet
func unmarkReferencedManifest(manifestArr []ManifestDel, markSet map[digest.Digest]struct{}) []ManifestDel {
filtered := make([]ManifestDel, 0)
for _, obj := range manifestArr {
if _, ok := markSet[obj.Digest]; !ok {
emit("manifest eligible for deletion: %s", obj)
filtered = append(filtered, obj)
}
}
return filtered
}
// markManifestReferences marks the manifest references
func markManifestReferences(dgst digest.Digest, manifestService distribution.ManifestService, ctx context.Context, ingester func(digest.Digest) bool) error {
manifest, err := manifestService.Get(ctx, dgst)
if err != nil {
return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err)
}
descriptors := manifest.References()
for _, descriptor := range descriptors {
// do not visit references if already marked
if ingester(descriptor.Digest) {
continue
}
if ok, _ := manifestService.Exists(ctx, descriptor.Digest); ok {
err := markManifestReferences(descriptor.Digest, manifestService, ctx, ingester)
if err != nil {
return err
}
}
}
return nil
}

View file

@ -7,7 +7,9 @@ import (
"github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3"
"github.com/distribution/distribution/v3/internal/dcontext" "github.com/distribution/distribution/v3/internal/dcontext"
"github.com/distribution/distribution/v3/manifest/ocischema"
"github.com/distribution/distribution/v3/registry/storage/driver" "github.com/distribution/distribution/v3/registry/storage/driver"
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
"github.com/distribution/distribution/v3/registry/storage/driver/inmemory" "github.com/distribution/distribution/v3/registry/storage/driver/inmemory"
"github.com/distribution/distribution/v3/testutil" "github.com/distribution/distribution/v3/testutil"
"github.com/distribution/reference" "github.com/distribution/reference"
@ -129,6 +131,29 @@ func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository)
} }
} }
func uploadRandomOCIImage(t *testing.T, repository distribution.Repository) image {
randomLayers, err := testutil.CreateRandomLayers(2)
if err != nil {
t.Fatalf("%v", err)
}
digests := []digest.Digest{}
for digest := range randomLayers {
digests = append(digests, digest)
}
manifest, err := testutil.MakeOCIManifest(repository, digests)
if err != nil {
t.Fatalf("%v", err)
}
manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers})
return image{
manifest: manifest,
manifestDigest: manifestDigest,
layers: randomLayers,
}
}
func TestNoDeletionNoEffect(t *testing.T) { func TestNoDeletionNoEffect(t *testing.T) {
ctx := dcontext.Background() ctx := dcontext.Background()
inmemoryDriver := inmemory.New() inmemoryDriver := inmemory.New()
@ -270,6 +295,202 @@ func TestDeleteManifestIfTagNotFound(t *testing.T) {
} }
} }
func TestDeleteManifestIndexWithDanglingReferences(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "deletemanifests")
manifestService, _ := repo.Manifests(ctx)
image1 := uploadRandomOCIImage(t, repo)
image2 := uploadRandomOCIImage(t, repo)
ii, _ := ocischema.FromDescriptors([]distribution.Descriptor{
{Digest: image1.manifestDigest}, {Digest: image2.manifestDigest},
}, map[string]string{})
id, err := manifestService.Put(ctx, ii)
if err != nil {
t.Fatalf("manifest upload failed: %v", err)
}
err = repo.Tags(ctx).Tag(ctx, "test", distribution.Descriptor{Digest: id})
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
// delete image2 => ii has a dangling reference
err = manifestService.Delete(ctx, image2.manifestDigest)
if err != nil {
t.Fatalf("Failed to delete image: %v", err)
}
before1 := allBlobs(t, registry)
before2 := allManifests(t, manifestService)
// run GC (should not remove anything because of tag)
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after1 := allBlobs(t, registry)
after2 := allManifests(t, manifestService)
if len(before1) == len(after1) {
t.Fatalf("Garbage collection did not affect blobs storage: %d == %d", len(before1), len(after1))
}
if len(before2) != len(after2) {
t.Fatalf("Garbage collection affected manifest storage: %d != %d", len(before2), len(after2))
}
}
func TestDeleteManifestIndexIfTagNotFound(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "deletemanifests")
manifestService, _ := repo.Manifests(ctx)
image1 := uploadRandomOCIImage(t, repo)
image2 := uploadRandomOCIImage(t, repo)
ii, _ := ocischema.FromDescriptors([]distribution.Descriptor{
{Digest: image1.manifestDigest}, {Digest: image2.manifestDigest},
}, map[string]string{})
d4, err := manifestService.Put(ctx, ii)
if err != nil {
t.Fatalf("manifest upload failed: %v", err)
}
err = repo.Tags(ctx).Tag(ctx, "test", distribution.Descriptor{Digest: d4})
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
before1 := allBlobs(t, registry)
before2 := allManifests(t, manifestService)
// run GC (should not remove anything because of tag)
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
beforeUntag1 := allBlobs(t, registry)
beforeUntag2 := allManifests(t, manifestService)
if len(before1) != len(beforeUntag1) {
t.Fatalf("Garbage collection affected blobs storage: %d != %d", len(before1), len(beforeUntag1))
}
if len(before2) != len(beforeUntag2) {
t.Fatalf("Garbage collection affected manifest storage: %d != %d", len(before2), len(beforeUntag2))
}
err = repo.Tags(ctx).Untag(ctx, "test")
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
// Run GC (removes everything because no manifests with tags exist)
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after1 := allBlobs(t, registry)
after2 := allManifests(t, manifestService)
if len(beforeUntag1) == len(after1) {
t.Fatalf("Garbage collection did not affect blobs storage: %d == %d", len(beforeUntag1), len(after1))
}
if len(beforeUntag2) == len(after2) {
t.Fatalf("Garbage collection did not affect manifest storage: %d == %d", len(beforeUntag2), len(after2))
}
}
func TestGCWithUnusedLayerLinkPath(t *testing.T) {
ctx := dcontext.Background()
d := inmemory.New()
registry := createRegistry(t, d)
repo := makeRepository(t, registry, "unusedlayerlink")
image := uploadRandomSchema2Image(t, repo)
for dgst := range image.layers {
layerLinkPath, err := pathFor(layerLinkPathSpec{name: "unusedlayerlink", digest: dgst})
if err != nil {
t.Fatal(err)
}
fileInfo, err := d.Stat(ctx, layerLinkPath)
if err != nil {
t.Fatal(err)
}
if fileInfo == nil {
t.Fatalf("layer link path %s not found", layerLinkPath)
}
}
err := MarkAndSweep(dcontext.Background(), d, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("got error: %v, expected nil", err)
}
for dgst := range image.layers {
layerLinkPath, err := pathFor(layerLinkPathSpec{name: "unusedlayerlink", digest: dgst})
if err != nil {
t.Fatal(err)
}
_, err = d.Stat(ctx, layerLinkPath)
if _, ok := err.(storagedriver.PathNotFoundError); !ok {
t.Fatalf("layer link path %s should be not found", layerLinkPath)
}
}
}
func TestGCWithUnknownRepository(t *testing.T) {
ctx := dcontext.Background()
d := inmemory.New()
registry := createRegistry(t, d)
repo := makeRepository(t, registry, "nonexistentrepo")
image := uploadRandomSchema2Image(t, repo)
err := repo.Tags(ctx).Tag(ctx, "image", distribution.Descriptor{Digest: image.manifestDigest})
if err != nil {
t.Fatalf("Failed to tag descriptor: %v", err)
}
// Simulate a missing _manifests/tags directory
manifestTagsPath, err := pathFor(manifestTagsPathSpec{"nonexistentrepo"})
if err != nil {
t.Fatal(err)
}
err = d.Delete(ctx, manifestTagsPath)
if err != nil {
t.Fatal(err)
}
err = MarkAndSweep(dcontext.Background(), d, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("got error: %v, expected nil", err)
}
}
func TestGCWithMissingManifests(t *testing.T) { func TestGCWithMissingManifests(t *testing.T) {
ctx := dcontext.Background() ctx := dcontext.Background()
d := inmemory.New() d := inmemory.New()
@ -479,3 +700,225 @@ func TestOrphanBlobDeleted(t *testing.T) {
} }
} }
} }
func TestTaggedManifestlistWithUntaggedManifest(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "foo/taggedlist/untaggedmanifest")
manifestService, err := repo.Manifests(ctx)
if err != nil {
t.Fatalf("%v", err)
}
image1 := uploadRandomSchema2Image(t, repo)
image2 := uploadRandomSchema2Image(t, repo)
// construct a manifestlist to reference manifests that is not tagged.
blobstatter := registry.BlobStatter()
manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{
image1.manifestDigest, image2.manifestDigest,
})
if err != nil {
t.Fatalf("Failed to make manifest list: %v", err)
}
dgst, err := manifestService.Put(ctx, manifestList)
if err != nil {
t.Fatalf("Failed to add manifest list: %v", err)
}
err = repo.Tags(ctx).Tag(ctx, "test", distribution.Descriptor{Digest: dgst})
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
before := allBlobs(t, registry)
// Run GC
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after := allBlobs(t, registry)
if len(before) != len(after) {
t.Fatalf("Garbage collection affected storage: %d != %d", len(before), len(after))
}
if _, ok := after[image1.manifestDigest]; !ok {
t.Fatalf("First manifest is missing")
}
if _, ok := after[image2.manifestDigest]; !ok {
t.Fatalf("Second manifest is missing")
}
if _, ok := after[dgst]; !ok {
t.Fatalf("Manifest list is missing")
}
}
func TestUnTaggedManifestlistWithUntaggedManifest(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "foo/untaggedlist/untaggedmanifest")
manifestService, err := repo.Manifests(ctx)
if err != nil {
t.Fatalf("%v", err)
}
image1 := uploadRandomSchema2Image(t, repo)
image2 := uploadRandomSchema2Image(t, repo)
// construct a manifestlist to reference manifests that is not tagged.
blobstatter := registry.BlobStatter()
manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{
image1.manifestDigest, image2.manifestDigest,
})
if err != nil {
t.Fatalf("Failed to make manifest list: %v", err)
}
_, err = manifestService.Put(ctx, manifestList)
if err != nil {
t.Fatalf("Failed to add manifest list: %v", err)
}
// Run GC
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after := allBlobs(t, registry)
if len(after) != 0 {
t.Fatalf("Garbage collection affected storage: %d != %d", len(after), 0)
}
}
func TestUnTaggedManifestlistWithTaggedManifest(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "foo/untaggedlist/taggedmanifest")
manifestService, err := repo.Manifests(ctx)
if err != nil {
t.Fatalf("%v", err)
}
image1 := uploadRandomSchema2Image(t, repo)
image2 := uploadRandomSchema2Image(t, repo)
err = repo.Tags(ctx).Tag(ctx, "image1", distribution.Descriptor{Digest: image1.manifestDigest})
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
err = repo.Tags(ctx).Tag(ctx, "image2", distribution.Descriptor{Digest: image2.manifestDigest})
if err != nil {
t.Fatalf("Failed to delete tag: %v", err)
}
// construct a manifestlist to reference manifests that is tagged.
blobstatter := registry.BlobStatter()
manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{
image1.manifestDigest, image2.manifestDigest,
})
if err != nil {
t.Fatalf("Failed to make manifest list: %v", err)
}
dgst, err := manifestService.Put(ctx, manifestList)
if err != nil {
t.Fatalf("Failed to add manifest list: %v", err)
}
// Run GC
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after := allBlobs(t, registry)
afterManifests := allManifests(t, manifestService)
if _, ok := after[dgst]; ok {
t.Fatalf("Untagged manifestlist still exists")
}
if _, ok := afterManifests[image1.manifestDigest]; !ok {
t.Fatalf("First manifest is missing")
}
if _, ok := afterManifests[image2.manifestDigest]; !ok {
t.Fatalf("Second manifest is missing")
}
}
func TestTaggedManifestlistWithDeletedReference(t *testing.T) {
ctx := dcontext.Background()
inmemoryDriver := inmemory.New()
registry := createRegistry(t, inmemoryDriver)
repo := makeRepository(t, registry, "foo/untaggedlist/deleteref")
manifestService, err := repo.Manifests(ctx)
if err != nil {
t.Fatalf("%v", err)
}
image1 := uploadRandomSchema2Image(t, repo)
image2 := uploadRandomSchema2Image(t, repo)
// construct a manifestlist to reference manifests that is deleted.
blobstatter := registry.BlobStatter()
manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{
image1.manifestDigest, image2.manifestDigest,
})
if err != nil {
t.Fatalf("Failed to make manifest list: %v", err)
}
_, err = manifestService.Put(ctx, manifestList)
if err != nil {
t.Fatalf("Failed to add manifest list: %v", err)
}
err = manifestService.Delete(ctx, image1.manifestDigest)
if err != nil {
t.Fatalf("Failed to delete image: %v", err)
}
err = manifestService.Delete(ctx, image2.manifestDigest)
if err != nil {
t.Fatalf("Failed to delete image: %v", err)
}
// Run GC
err = MarkAndSweep(dcontext.Background(), inmemoryDriver, registry, GCOpts{
DryRun: false,
RemoveUntagged: true,
})
if err != nil {
t.Fatalf("Failed mark and sweep: %v", err)
}
after := allBlobs(t, registry)
if len(after) != 0 {
t.Fatalf("Garbage collection affected storage: %d != %d", len(after), 0)
}
}

Some files were not shown because too many files have changed in this diff Show more