feature/11-update_master_to_beta_release_commits #12
119 changed files with 4211 additions and 1321 deletions
48
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
12
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
12
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
8
.github/dependabot.yml
vendored
Normal 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
61
.github/labeler.yml
vendored
Normal 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
|
43
.github/workflows/build.yml
vendored
43
.github/workflows/build.yml
vendored
|
@ -27,18 +27,18 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
go:
|
||||
- 1.20.12
|
||||
- 1.21.5
|
||||
- 1.21.8
|
||||
- 1.22.1
|
||||
target:
|
||||
- test-coverage
|
||||
- test-cloud-storage
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
-
|
||||
|
@ -47,7 +47,7 @@ jobs:
|
|||
make ${{ matrix.target }}
|
||||
-
|
||||
name: Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
directory: ./
|
||||
|
||||
|
@ -62,13 +62,13 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.DOCKERHUB_SLUG }}
|
||||
|
@ -94,43 +94,53 @@ jobs:
|
|||
org.opencontainers.image.description=The toolkit to pack, ship, store, and distribute container content
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Log in to GitHub Container registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
-
|
||||
name: Build artifacts
|
||||
uses: docker/bake-action@v2
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
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: |
|
||||
mv ./bin/**/* ./bin/
|
||||
tree -nh ./bin
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
with:
|
||||
name: registry
|
||||
path: ./bin/*
|
||||
if-no-files-found: error
|
||||
-
|
||||
name: Build image
|
||||
uses: docker/bake-action@v2
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
|
@ -145,6 +155,7 @@ jobs:
|
|||
draft: true
|
||||
files: |
|
||||
bin/*.tar.gz
|
||||
bin/*.provenance.json
|
||||
bin/*.sha256
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
-
|
||||
|
@ -44,12 +44,12 @@ jobs:
|
|||
git checkout HEAD^2
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3.22.12
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3.22.12
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3.22.12
|
||||
|
|
6
.github/workflows/conformance.yml
vendored
6
.github/workflows/conformance.yml
vendored
|
@ -17,12 +17,12 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Build image
|
||||
uses: docker/bake-action@v2
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
targets: image-local
|
||||
-
|
||||
|
@ -49,7 +49,7 @@ jobs:
|
|||
run: mkdir -p .out/ && mv {report.html,junit.xml} .out/
|
||||
-
|
||||
name: Upload test results
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4.3.0
|
||||
with:
|
||||
name: oci-test-results-${{ github.sha }}
|
||||
path: .out/
|
||||
|
|
2
.github/workflows/dockerhub-readme.yml
vendored
2
.github/workflows/dockerhub-readme.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Update Docker Hub README
|
||||
uses: peter-evans/dockerhub-description@v3
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
|
13
.github/workflows/docs.yml
vendored
13
.github/workflows/docs.yml
vendored
|
@ -26,27 +26,26 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v3
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build docs
|
||||
uses: docker/bake-action@v3
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
files: |
|
||||
docker-bake.hcl
|
||||
targets: docs-export
|
||||
provenance: false
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=docs
|
||||
*.cache-to=type=gha,scope=docs,mode=max
|
||||
env:
|
||||
DOCS_BASEURL: ${{ steps.pages.outputs.base_path }}
|
||||
- name: Fix permissions
|
||||
run: |
|
||||
chmod -c -R +rX "./build/docs" | while read line; do
|
||||
echo "::warning title=Invalid file permissions automatically fixed::$line"
|
||||
done
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./build/docs
|
||||
|
||||
|
@ -70,4 +69,4 @@ jobs:
|
|||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
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
|
||||
|
|
6
.github/workflows/e2e.yml
vendored
6
.github/workflows/e2e.yml
vendored
|
@ -20,12 +20,12 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Build image
|
||||
uses: docker/bake-action@v2
|
||||
uses: docker/bake-action@v4
|
||||
with:
|
||||
targets: image-local
|
||||
-
|
||||
|
@ -42,7 +42,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
|
|
4
.github/workflows/fossa.yml
vendored
4
.github/workflows/fossa.yml
vendored
|
@ -17,9 +17,9 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run FOSSA scan and upload build data
|
||||
uses: fossa-contrib/fossa-action@v2
|
||||
uses: fossa-contrib/fossa-action@v3
|
||||
with:
|
||||
fossa-api-key: cac3dc8d4f2ba86142f6c0f2199a160f
|
||||
|
|
19
.github/workflows/label.yaml
vendored
Normal file
19
.github/workflows/label.yaml
vendored
Normal 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
|
8
.github/workflows/scorecards.yml
vendored
8
.github/workflows/scorecards.yml
vendored
|
@ -22,12 +22,12 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846 # tag=v3.0.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # tag=v4.1.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@99c53751e09b9529366343771cc321ec74e9bd3d # tag=v2.0.6
|
||||
uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # tag=v2.3.1
|
||||
with:
|
||||
results_file: results.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
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # tag=v3.0.0
|
||||
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # tag=v4.3.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
|
@ -54,7 +54,7 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- 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:
|
||||
sarif_file: results.sarif
|
||||
|
||||
|
|
2
.github/workflows/validate.yml
vendored
2
.github/workflows/validate.yml
vendored
|
@ -29,7 +29,7 @@ jobs:
|
|||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Run
|
||||
run: |
|
||||
|
|
|
@ -6,7 +6,7 @@ linters:
|
|||
- goimports
|
||||
- revive
|
||||
- ineffassign
|
||||
- vet
|
||||
- govet
|
||||
- unused
|
||||
- misspell
|
||||
- bodyclose
|
||||
|
@ -22,7 +22,7 @@ linters-settings:
|
|||
- name: unused-parameter
|
||||
disabled: true
|
||||
|
||||
run:
|
||||
issues:
|
||||
deadline: 2m
|
||||
skip-dirs:
|
||||
exlude-dirs:
|
||||
- vendor
|
||||
|
|
224
.mailmap
224
.mailmap
|
@ -1,32 +1,194 @@
|
|||
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@users.noreply.github.com>
|
||||
Stephen J Day <stephen.day@docker.com> Stephen Day <stevvooe@gmail.com>
|
||||
Olivier Gambier <olivier@docker.com> Olivier Gambier <dmp42@users.noreply.github.com>
|
||||
Brian Bland <brian.bland@docker.com> Brian Bland <r4nd0m1n4t0r@gmail.com>
|
||||
Brian Bland <brian.bland@docker.com> Brian Bland <brian.t.bland@gmail.com>
|
||||
Josh Hawn <josh.hawn@docker.com> Josh Hawn <jlhawn@berkeley.edu>
|
||||
Richard Scothern <richard.scothern@docker.com> Richard <richard.scothern@gmail.com>
|
||||
Richard Scothern <richard.scothern@docker.com> Richard Scothern <richard.scothern@gmail.com>
|
||||
Andrew Meredith <andymeredith@gmail.com> Andrew Meredith <kendru@users.noreply.github.com>
|
||||
harche <p.harshal@gmail.com> harche <harche@users.noreply.github.com>
|
||||
Jessie Frazelle <jessie@docker.com> <jfrazelle@users.noreply.github.com>
|
||||
Sharif Nassar <sharif@mrwacky.com> Sharif Nassar <mrwacky42@users.noreply.github.com>
|
||||
Sven Dowideit <SvenDowideit@home.org.au> Sven Dowideit <SvenDowideit@users.noreply.github.com>
|
||||
Vincent Giersch <vincent.giersch@ovh.net> Vincent Giersch <vincent@giersch.fr>
|
||||
davidli <wenquan.li@hp.com> davidli <wenquan.li@hpe.com>
|
||||
Omer Cohen <git@omer.io> Omer Cohen <git@omerc.net>
|
||||
Eric Yang <windfarer@gmail.com> Eric Yang <Windfarer@users.noreply.github.com>
|
||||
Nikita Tarasov <nikita@mygento.ru> Nikita <luckyraul@users.noreply.github.com>
|
||||
Yu Wang <yuwa@microsoft.com> yuwaMSFT2 <yuwa@microsoft.com>
|
||||
Aaron Lehmann <alehmann@netflix.com>
|
||||
Aaron Lehmann <alehmann@netflix.com> <aaron.lehmann@docker.com>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
|
||||
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
|
||||
Alexander Morozov <lk4d4math@gmail.com>
|
||||
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
|
||||
Anders Ingemann <aim@orbit.online>
|
||||
Andrew Meredith <andymeredith@gmail.com>
|
||||
Andrew Meredith <andymeredith@gmail.com> <kendru@users.noreply.github.com>
|
||||
Andrey Smirnov <andrey.smirnov@siderolabs.com>
|
||||
Andrii Soldatenko <andrii.soldatenko@gmail.com>
|
||||
Andrii Soldatenko <andrii.soldatenko@gmail.com> <andrii.soldatenko@dynatrace.com>
|
||||
Anthony Ramahay <thewolt@gmail.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <amurdaca@redhat.com>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <me@runcom.ninja>
|
||||
Antonio Murdaca <antonio.murdaca@gmail.com> <runcom@linux.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>
|
||||
Olivier Gambier <olivier@docker.com> dmp <dmp@loaner.local>
|
||||
Olivier Gambier <olivier@docker.com> Olivier <o+github@gambier.email>
|
||||
Olivier Gambier <olivier@docker.com> Olivier <dmp42@users.noreply.github.com>
|
||||
Elsan Li 李楠 <elsanli@tencent.com> elsanli(李楠) <elsanli@tencent.com>
|
||||
Rui Cao <ruicao@alauda.io> ruicao <ruicao@alauda.io>
|
||||
Gwendolynne Barr <gwendolynne.barr@docker.com> gbarr01 <gwendolynne.barr@docker.com>
|
||||
Haibing Zhou 周海兵 <zhouhaibing089@gmail.com> zhouhaibing089 <zhouhaibing089@gmail.com>
|
||||
Feng Honglin <tifayuki@gmail.com> tifayuki <tifayuki@gmail.com>
|
||||
Helen Xie <xieyulin821@harmonycloud.cn> Helen-xie <xieyulin821@harmonycloud.cn>
|
||||
Mike Brown <brownwm@us.ibm.com> Mike Brown <mikebrow@users.noreply.github.com>
|
||||
Manish Tomar <manish.tomar@docker.com> Manish Tomar <manishtomar@users.noreply.github.com>
|
||||
Sakeven Jiang <jc5930@sina.cn> sakeven <jc5930@sina.cn>
|
||||
baojiangnan <baojiangnan@meituan.com>
|
||||
baojiangnan <baojiangnan@meituan.com> <baojn1998@163.com>
|
||||
erezrokah <erezrokah@users.noreply.github.com>
|
||||
goodactive <goodactive@qq.com>
|
||||
gotgelf <gotgelf@gmail.com>
|
||||
guoguangwu <guoguangwug@gmail.com>
|
||||
harche <p.harshal@gmail.com>
|
||||
harche <p.harshal@gmail.com> <harche@users.noreply.github.com>
|
||||
icefed <zlwangel@gmail.com>
|
||||
oliver-goetz <o.goetz@sap.com>
|
||||
xiaoxiangxianzi <zhaoyizheng@outlook.com>
|
||||
|
|
530
AUTHORS
Normal file
530
AUTHORS
Normal 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>
|
10
Dockerfile
10
Dockerfile
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.5
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.22.4
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG XX_VERSION=1.2.1
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
@ -16,7 +16,7 @@ FROM base AS version
|
|||
ARG PKG=github.com/distribution/distribution/v3
|
||||
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); \
|
||||
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;
|
||||
|
||||
FROM base AS build
|
||||
|
@ -52,9 +52,9 @@ COPY --from=releaser /out /
|
|||
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
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
|
||||
VOLUME ["/var/lib/registry"]
|
||||
EXPOSE 5000
|
||||
ENTRYPOINT ["registry"]
|
||||
CMD ["serve", "/etc/docker/registry/config.yml"]
|
||||
CMD ["serve", "/etc/distribution/config.yml"]
|
||||
|
|
10
Makefile
10
Makefile
|
@ -37,7 +37,7 @@ WHALE = "+"
|
|||
TESTFLAGS_RACE=
|
||||
GOFILES=$(shell find . -type f -name '*.go')
|
||||
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))
|
||||
|
||||
|
@ -45,7 +45,7 @@ BINARIES=$(addprefix bin/,$(COMMANDS))
|
|||
TESTFLAGS ?= -v $(TESTFLAGS_RACE)
|
||||
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
|
||||
|
||||
.PHONY: FORCE
|
||||
|
@ -86,6 +86,9 @@ vendor: ## update vendor
|
|||
mod-outdated: ## check outdated dependencies
|
||||
docker buildx bake $@
|
||||
|
||||
authors: ## generate authors
|
||||
docker buildx bake $@
|
||||
|
||||
##@ Test
|
||||
|
||||
test: ## run tests, except integration test with test.short
|
||||
|
@ -172,6 +175,9 @@ validate-git: ## validate git
|
|||
validate-vendor: ## validate vendor
|
||||
docker buildx bake $@
|
||||
|
||||
validate-authors: ## validate authors
|
||||
docker buildx bake $@
|
||||
|
||||
.PHONY: 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)
|
||||
|
|
|
@ -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" />
|
||||
</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)
|
||||
[![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)
|
||||
|
@ -27,7 +27,7 @@ This repository contains the following components:
|
|||
|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **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**. |
|
||||
| **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?
|
||||
|
||||
|
|
6
blobs.go
6
blobs.go
|
@ -140,12 +140,6 @@ type BlobDescriptorServiceFactory interface {
|
|||
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.
|
||||
type BlobProvider interface {
|
||||
// Get returns the entire blob identified by digest along with the descriptor.
|
||||
|
|
|
@ -12,6 +12,8 @@ storage:
|
|||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
tag:
|
||||
concurrencylimit: 8
|
||||
http:
|
||||
addr: :5000
|
||||
secret: asecretforlocaldevelopment
|
||||
|
@ -20,11 +22,10 @@ http:
|
|||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
redis:
|
||||
addr: localhost:6379
|
||||
pool:
|
||||
maxidle: 16
|
||||
maxactive: 64
|
||||
idletimeout: 300s
|
||||
addrs: [localhost:6379]
|
||||
maxidleconns: 16
|
||||
poolsize: 64
|
||||
connmaxidletime: 300s
|
||||
dialtimeout: 10ms
|
||||
readtimeout: 10ms
|
||||
writetimeout: 10ms
|
||||
|
|
|
@ -14,6 +14,8 @@ storage:
|
|||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: false
|
||||
tag:
|
||||
concurrencylimit: 8
|
||||
http:
|
||||
addr: :5000
|
||||
debug:
|
||||
|
|
|
@ -7,6 +7,8 @@ storage:
|
|||
blobdescriptor: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
tag:
|
||||
concurrencylimit: 8
|
||||
http:
|
||||
addr: :5000
|
||||
headers:
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
_ "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/redirect"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/middleware/rewrite"
|
||||
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
|
||||
)
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// 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 struct {
|
||||
// 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"`
|
||||
} `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"`
|
||||
|
||||
// Notifications specifies configuration about various endpoint to which
|
||||
|
@ -175,25 +183,7 @@ type Configuration struct {
|
|||
Proxy Proxy `yaml:"proxy,omitempty"`
|
||||
|
||||
// Validation configures validation options for the registry.
|
||||
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 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"`
|
||||
Validation Validation `yaml:"validation,omitempty"`
|
||||
|
||||
// Policy configures registry policy options.
|
||||
Policy struct {
|
||||
|
@ -271,44 +261,6 @@ type FileChecker struct {
|
|||
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.
|
||||
type HTTPChecker struct {
|
||||
// Timeout is the duration to wait before timing out the HTTP request
|
||||
|
@ -360,6 +312,13 @@ type Health struct {
|
|||
} `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
|
||||
// This is currently aliased to Configuration, as it is the current version
|
||||
type v0_1Configuration Configuration
|
||||
|
@ -435,6 +394,8 @@ func (storage Storage) Type() string {
|
|||
// allow configuration of delete
|
||||
case "redirect":
|
||||
// allow configuration of redirect
|
||||
case "tag":
|
||||
// allow configuration of tag
|
||||
default:
|
||||
storageType = append(storageType, k)
|
||||
}
|
||||
|
@ -448,6 +409,19 @@ func (storage Storage) Type() string {
|
|||
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
|
||||
func (storage Storage) Parameters() Parameters {
|
||||
return storage[storage.Type()]
|
||||
|
@ -476,6 +450,8 @@ func (storage *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
// allow configuration of delete
|
||||
case "redirect":
|
||||
// allow configuration of redirect
|
||||
case "tag":
|
||||
// allow configuration of tag
|
||||
default:
|
||||
types = append(types, k)
|
||||
}
|
||||
|
@ -630,6 +606,62 @@ type Proxy struct {
|
|||
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
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -39,6 +40,9 @@ var configStruct = Configuration{
|
|||
"url1": "https://foo.example.com",
|
||||
"path1": "/some-path",
|
||||
},
|
||||
"tag": Parameters{
|
||||
"concurrencylimit": 10,
|
||||
},
|
||||
},
|
||||
Auth: Auth{
|
||||
"silly": Parameters{
|
||||
|
@ -97,6 +101,9 @@ var configStruct = Configuration{
|
|||
HTTP2 struct {
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
} `yaml:"http2,omitempty"`
|
||||
H2C struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
} `yaml:"h2c,omitempty"`
|
||||
}{
|
||||
TLS: struct {
|
||||
Certificate string `yaml:"certificate,omitempty"`
|
||||
|
@ -121,25 +128,38 @@ var configStruct = Configuration{
|
|||
}{
|
||||
Disabled: false,
|
||||
},
|
||||
H2C: struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
}{
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
Redis: Redis{
|
||||
Addr: "localhost:6379",
|
||||
Options: redis.UniversalOptions{
|
||||
Addrs: []string{"localhost:6379"},
|
||||
Username: "alice",
|
||||
Password: "123456",
|
||||
DB: 1,
|
||||
Pool: struct {
|
||||
MaxIdle int `yaml:"maxidle,omitempty"`
|
||||
MaxActive int `yaml:"maxactive,omitempty"`
|
||||
IdleTimeout time.Duration `yaml:"idletimeout,omitempty"`
|
||||
}{
|
||||
MaxIdle: 16,
|
||||
MaxActive: 64,
|
||||
IdleTimeout: time.Second * 300,
|
||||
},
|
||||
MaxIdleConns: 16,
|
||||
PoolSize: 64,
|
||||
ConnMaxIdleTime: time.Second * 300,
|
||||
DialTimeout: time.Millisecond * 10,
|
||||
ReadTimeout: 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
|
||||
|
@ -159,6 +179,8 @@ storage:
|
|||
int1: 42
|
||||
url1: "https://foo.example.com"
|
||||
path1: "/some-path"
|
||||
tag:
|
||||
concurrencylimit: 10
|
||||
auth:
|
||||
silly:
|
||||
realm: silly
|
||||
|
@ -177,22 +199,31 @@ notifications:
|
|||
actions:
|
||||
- pull
|
||||
http:
|
||||
tls:
|
||||
clientcas:
|
||||
- /path/to/ca.pem
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
redis:
|
||||
addr: localhost:6379
|
||||
tls:
|
||||
certificate: /foo/cert.crt
|
||||
key: /foo/key.pem
|
||||
clientcas:
|
||||
- /path/to/ca.pem
|
||||
addrs: [localhost:6379]
|
||||
username: alice
|
||||
password: 123456
|
||||
password: "123456"
|
||||
db: 1
|
||||
pool:
|
||||
maxidle: 16
|
||||
maxactive: 64
|
||||
idletimeout: 300s
|
||||
maxidleconns: 16
|
||||
poolsize: 64
|
||||
connmaxidletime: 300s
|
||||
dialtimeout: 10ms
|
||||
readtimeout: 10ms
|
||||
writetimeout: 10ms
|
||||
validation:
|
||||
manifests:
|
||||
indexes:
|
||||
platforms: none
|
||||
`
|
||||
|
||||
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
|
||||
|
@ -222,6 +253,10 @@ notifications:
|
|||
http:
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
validation:
|
||||
manifests:
|
||||
indexes:
|
||||
platforms: none
|
||||
`
|
||||
|
||||
type ConfigSuite struct {
|
||||
|
@ -261,6 +296,7 @@ func (suite *ConfigSuite) TestParseSimple() {
|
|||
func (suite *ConfigSuite) TestParseInmemory() {
|
||||
suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}}
|
||||
suite.expectedConfig.Log.Fields = nil
|
||||
suite.expectedConfig.HTTP.TLS.ClientCAs = nil
|
||||
suite.expectedConfig.Redis = Redis{}
|
||||
|
||||
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.Notifications = Notifications{}
|
||||
suite.expectedConfig.HTTP.Headers = nil
|
||||
suite.expectedConfig.HTTP.TLS.ClientCAs = nil
|
||||
suite.expectedConfig.Redis = Redis{}
|
||||
suite.expectedConfig.Validation.Manifests.Indexes.Platforms = ""
|
||||
|
||||
// Note: this also tests that REGISTRY_STORAGE and
|
||||
// REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together
|
||||
|
@ -534,6 +572,9 @@ func copyConfig(config Configuration) *Configuration {
|
|||
for k, v := range config.Storage.Parameters() {
|
||||
configCopy.Storage.setParameter(k, v)
|
||||
}
|
||||
for k, v := range config.Storage.TagParameters() {
|
||||
configCopy.Storage.setTagParameter(k, v)
|
||||
}
|
||||
|
||||
configCopy.Auth = Auth{config.Auth.Type(): Parameters{}}
|
||||
for k, v := range config.Auth.Parameters() {
|
||||
|
@ -547,8 +588,20 @@ func copyConfig(config Configuration) *Configuration {
|
|||
for k, v := range config.HTTP.Headers {
|
||||
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.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
|
||||
}
|
||||
|
|
|
@ -39,11 +39,7 @@ target "update-vendor" {
|
|||
target "mod-outdated" {
|
||||
dockerfile = "./dockerfiles/vendor.Dockerfile"
|
||||
target = "outdated"
|
||||
args = {
|
||||
// used to invalidate cache for outdated run stage
|
||||
// can be dropped when https://github.com/moby/buildkit/issues/1213 fixed
|
||||
_RANDOM = uuidv4()
|
||||
}
|
||||
no-cache-filter = ["outdated"]
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
|
@ -95,15 +91,8 @@ target "image-all" {
|
|||
]
|
||||
}
|
||||
|
||||
variable "DOCS_BASEURL" {
|
||||
default = null
|
||||
}
|
||||
|
||||
target "_common_docs" {
|
||||
dockerfile = "./dockerfiles/docs.Dockerfile"
|
||||
args = {
|
||||
DOCS_BASEURL = DOCS_BASEURL
|
||||
}
|
||||
}
|
||||
|
||||
target "docs-export" {
|
||||
|
@ -124,3 +113,15 @@ target "docs-test" {
|
|||
target = "test"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
||||
target "authors" {
|
||||
dockerfile = "./dockerfiles/authors.Dockerfile"
|
||||
target = "update"
|
||||
output = ["."]
|
||||
}
|
||||
|
||||
target "validate-authors" {
|
||||
dockerfile = "./dockerfiles/authors.Dockerfile"
|
||||
target = "validate"
|
||||
output = ["type=cacheonly"]
|
||||
}
|
||||
|
|
34
dockerfiles/authors.Dockerfile
Normal file
34
dockerfiles/authors.Dockerfile
Normal 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
|
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.21.5
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.22.4
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
RUN apk add --no-cache git
|
||||
|
@ -16,9 +16,8 @@ COPY --from=hugo $GOPATH/bin/hugo /bin/hugo
|
|||
WORKDIR /src
|
||||
|
||||
FROM build-base AS build
|
||||
ARG DOCS_BASEURL=/
|
||||
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
|
||||
COPY docs .
|
||||
|
@ -29,8 +28,12 @@ FROM scratch AS out
|
|||
COPY --from=build /out /
|
||||
|
||||
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
|
||||
COPY --from=build /out ./public
|
||||
ADD docs/.htmltest.yml .htmltest.yml
|
||||
RUN --mount=type=cache,target=tmp/.htmltest \
|
||||
htmltest
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.20.12
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.22.4
|
||||
ARG ALPINE_VERSION=3.20
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS base
|
||||
RUN apk add --no-cache git gpg
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.20.12
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GOLANGCI_LINT_VERSION=v1.55.2
|
||||
ARG GO_VERSION=1.22.4
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG GOLANGCI_LINT_VERSION=v1.59.1
|
||||
ARG BUILDTAGS=""
|
||||
|
||||
FROM golangci/golangci-lint:${GOLANGCI_LINT_VERSION}-alpine AS golangci-lint
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.20.12
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.22.4
|
||||
ARG ALPINE_VERSION=3.20
|
||||
ARG MODOUTDATED_VERSION=v0.8.0
|
||||
|
||||
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 base AS outdated
|
||||
ARG _RANDOM
|
||||
RUN --mount=target=.,ro \
|
||||
--mount=target=/go/pkg/mod,type=cache \
|
||||
--mount=from=go-mod-outdated,source=/home/go-mod-outdated,target=/usr/bin/go-mod-outdated \
|
||||
|
|
|
@ -50,7 +50,7 @@ specify it in the `docker run` command:
|
|||
|
||||
```bash
|
||||
$ 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
|
||||
```
|
||||
|
||||
|
@ -141,6 +141,8 @@ storage:
|
|||
usedualstack: false
|
||||
loglevel: debug
|
||||
inmemory: # This driver takes no parameters
|
||||
tag:
|
||||
concurrencylimit: 8
|
||||
delete:
|
||||
enabled: false
|
||||
redirect:
|
||||
|
@ -166,6 +168,10 @@ auth:
|
|||
service: token-service
|
||||
issuer: registry-token-issuer
|
||||
rootcertbundle: /root/certs/bundle
|
||||
jwks: /path/to/jwks
|
||||
signingalgorithms:
|
||||
- EdDSA
|
||||
- HS256
|
||||
htpasswd:
|
||||
realm: basic-realm
|
||||
path: /path/to/htpasswd
|
||||
|
@ -220,6 +226,8 @@ http:
|
|||
X-Content-Type-Options: [nosniff]
|
||||
http2:
|
||||
disabled: false
|
||||
h2c:
|
||||
enabled: false
|
||||
notifications:
|
||||
events:
|
||||
includereferences: true
|
||||
|
@ -239,16 +247,20 @@ notifications:
|
|||
actions:
|
||||
- pull
|
||||
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
|
||||
db: 0
|
||||
dialtimeout: 10ms
|
||||
readtimeout: 10ms
|
||||
writetimeout: 10ms
|
||||
pool:
|
||||
maxidle: 16
|
||||
maxactive: 64
|
||||
idletimeout: 300s
|
||||
maxidleconns: 16
|
||||
poolsize: 64
|
||||
connmaxidletime: 300s
|
||||
tls:
|
||||
enabled: false
|
||||
health:
|
||||
|
@ -284,6 +296,11 @@ validation:
|
|||
- ^https?://([^/]+\.)*example\.com/
|
||||
deny:
|
||||
- ^https?://www\.example\.com/
|
||||
indexes:
|
||||
platforms: List
|
||||
platformlist:
|
||||
- architecture: amd64
|
||||
os: linux
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
| 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). |
|
||||
| `azure` | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](/storage-drivers/azure). |
|
||||
| `gcs` | Uses Google Cloud Storage. See the [driver's reference documentation](/storage-drivers/gcs). |
|
||||
| `s3` | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](/storage-drivers/s3). |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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.md). |
|
||||
| `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.md). |
|
||||
|
||||
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
|
||||
[`filesystem` driver](/storage-drivers/filesystem)
|
||||
[`filesystem` driver](../storage-drivers/filesystem.md)
|
||||
on a ramdisk.
|
||||
|
||||
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
|
||||
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`
|
||||
|
||||
The `redirect` subsection provides configuration for managing redirects from
|
||||
|
@ -548,6 +585,11 @@ auth:
|
|||
service: token-service
|
||||
issuer: registry-token-issuer
|
||||
rootcertbundle: /root/certs/bundle
|
||||
jwks: /path/to/jwks
|
||||
signingalgorithms:
|
||||
- EdDSA
|
||||
- HS256
|
||||
- ES512
|
||||
htpasswd:
|
||||
realm: basic-realm
|
||||
path: /path/to/htpasswd
|
||||
|
@ -584,16 +626,48 @@ the registry. It is an established authentication paradigm with a high degree of
|
|||
security.
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------------------------------------------------|
|
||||
|----------------------|----------|-------------------------------------------------------|
|
||||
| `realm` | yes | The realm in which the registry server authenticates. |
|
||||
| `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. |
|
||||
| `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
|
||||
[specification](/spec/auth/token).
|
||||
[specification](../spec/auth/token.md).
|
||||
|
||||
### `htpasswd`
|
||||
|
||||
|
@ -724,6 +798,8 @@ http:
|
|||
X-Content-Type-Options: [nosniff]
|
||||
http2:
|
||||
disabled: false
|
||||
h2c:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
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.
|
||||
If `tls` is not configured this option is ignored. To enable HTTP/2 over non TLS connections use `h2c` instead.
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------------------------------------------------|
|
||||
| `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`
|
||||
|
||||
```yaml
|
||||
|
@ -937,72 +1024,46 @@ The `events` structure configures the information provided in event notification
|
|||
|
||||
## `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
|
||||
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
|
||||
db: 0
|
||||
dialtimeout: 10ms
|
||||
readtimeout: 10ms
|
||||
writetimeout: 10ms
|
||||
pool:
|
||||
maxidle: 16
|
||||
maxactive: 64
|
||||
idletimeout: 300s
|
||||
tls:
|
||||
enabled: false
|
||||
maxidleconns: 16
|
||||
poolsize: 64
|
||||
connmaxidletime: 300s
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
```yaml
|
||||
|
@ -1100,7 +1161,7 @@ proxy:
|
|||
|
||||
The `proxy` structure allows a registry to be configured as a pull-through cache
|
||||
to Docker Hub. See
|
||||
[mirror](/recipes/mirror)
|
||||
[mirror](../recipes/mirror.md)
|
||||
for more information. Pushing to a registry configured as a pull-through cache
|
||||
is unsupported.
|
||||
|
||||
|
@ -1122,14 +1183,14 @@ username (such as `batman`) and the password for that username.
|
|||
|
||||
```yaml
|
||||
validation:
|
||||
manifests:
|
||||
urls:
|
||||
allow:
|
||||
- ^https?://([^/]+\.)*example\.com/
|
||||
deny:
|
||||
- ^https?://www\.example\.com/
|
||||
disabled: false
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
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`
|
||||
|
||||
```yaml
|
||||
validation:
|
||||
manifests:
|
||||
urls:
|
||||
allow:
|
||||
- ^https?://([^/]+\.)*example\.com/
|
||||
deny:
|
||||
- ^https?://www\.example\.com/
|
||||
```
|
||||
|
||||
The `allow` and `deny` options are each a list of
|
||||
[regular expressions](https://pkg.go.dev/regexp/syntax) that restrict the URLs in
|
||||
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
|
||||
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
|
||||
|
||||
You can use this simple example for local development:
|
||||
|
|
|
@ -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
|
||||
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
|
||||
[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
|
||||
> 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
|
||||
> continue to the [configuration guide](../configuration) to deploy a
|
||||
> continue to the [configuration guide](configuration.md) to deploy a
|
||||
> production-ready 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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -166,8 +166,8 @@ $ docker run -d \
|
|||
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
|
||||
bucket, Google Cloud Platform, or on another storage back-end by using
|
||||
[storage drivers](/storage-drivers). For more information, see
|
||||
[storage configuration options](../configuration#storage).
|
||||
[storage drivers](../storage-drivers/_index.md). For more information, see
|
||||
[storage configuration options](configuration.md#storage).
|
||||
|
||||
## 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
|
||||
[https://letsencrypt.org/how-it-works/](https://letsencrypt.org/how-it-works/)
|
||||
and the relevant section of the
|
||||
[registry configuration](../configuration#letsencrypt).
|
||||
[registry configuration](configuration.md#letsencrypt).
|
||||
|
||||
### Use an insecure registry (testing only)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -462,20 +462,20 @@ using htpasswd, all authentication attempts will fail.
|
|||
{{< hint type=note title="X509 errors" >}}
|
||||
X509 errors usually indicate that you are attempting to use
|
||||
a self-signed certificate without configuring the Docker daemon correctly.
|
||||
See [run an insecure registry](../insecure).
|
||||
See [run an insecure registry](insecure.md).
|
||||
{{< /hint >}}
|
||||
|
||||
### More advanced authentication
|
||||
|
||||
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
|
||||
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
|
||||
the registry's integration into your global authorization and authentication
|
||||
systems. Refer to the following [background information](/spec/auth/token) and
|
||||
[configuration information here](../configuration#auth).
|
||||
systems. Refer to the following [background information](../spec/auth/token.md) and
|
||||
[configuration information here](configuration.md#auth).
|
||||
|
||||
This approach requires you to implement your own authentication system or
|
||||
leverage a third-party implementation.
|
||||
|
@ -572,9 +572,9 @@ artifacts.
|
|||
|
||||
More specific and advanced information is available in the following sections:
|
||||
|
||||
- [Configuration reference](../configuration)
|
||||
- [Working with notifications](../notifications)
|
||||
- [Advanced "recipes"](/recipes)
|
||||
- [Registry API](/spec/api)
|
||||
- [Storage driver model](/storage-drivers)
|
||||
- [Token authentication](/spec/auth/token)
|
||||
- [Configuration reference](configuration.md)
|
||||
- [Working with notifications](notifications.md)
|
||||
- [Advanced "recipes"](../recipes/_index.md)
|
||||
- [Registry API](../spec/api.md)
|
||||
- [Storage driver model](../storage-drivers/_index.md)
|
||||
- [Token authentication](../spec/auth/token.md)
|
||||
|
|
|
@ -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
|
||||
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
|
||||
to the layer. As long as a layer is referenced by one manifest, it cannot be garbage
|
||||
collected.
|
||||
|
||||
Manifests and layers can be `deleted` with the registry API (refer to the API
|
||||
documentation [here](/spec/api#deleting-a-layer) and
|
||||
[here](/spec/api#deleting-an-image) for details). This API removes references
|
||||
documentation [here](../spec/api.md#deleting-a-layer) and
|
||||
[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
|
||||
unable to be read via the API.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
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
|
||||
depends on your OS.
|
||||
|
|
|
@ -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
|
||||
queues and dispatches events to [_Endpoints_](#endpoints).
|
||||
|
||||
![Workflow of registry notifications](../../images/notifications.png)
|
||||
![Workflow of registry notifications](/distribution/images/notifications.png)
|
||||
|
||||
## 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
|
||||
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
|
||||
upon startup:
|
||||
|
|
|
@ -12,7 +12,7 @@ Usually, that includes enterprise setups using LDAP/AD on the backend and a SSO
|
|||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ The following table shows examples of allowed and disallowed mirror URLs.
|
|||
|
||||
> **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
|
||||
|
||||
|
@ -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
|
||||
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.
|
||||
|
||||
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
|
||||
> 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
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ mechanism fronting their internal http portal.
|
|||
|
||||
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).
|
||||
[basic auth registry feature](../about/deploying.md#native-basic-auth).
|
||||
|
||||
### Solution
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ keywords: registry, service, images, repository, json
|
|||
|
||||
# Docker Registry Reference
|
||||
|
||||
* [HTTP API V2](api)
|
||||
* [Storage Driver](/storage-drivers/)
|
||||
* [Token Authentication Specification](auth/token)
|
||||
* [Token Authentication Implementation](auth/jwt)
|
||||
* [HTTP API V2](api.md)
|
||||
* [Storage Driver](../storage-drivers/_index.md)
|
||||
* [Token Authentication Specification](auth/token.md)
|
||||
* [Token Authentication Implementation](auth/jwt.md)
|
||||
|
|
|
@ -416,7 +416,7 @@ reference may include a tag or digest.
|
|||
|
||||
The client should include an Accept header indicating which manifest content
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
> for more details, see: [compatibility](/about/compatibility#content-addressable-storage-cas)
|
||||
> for more details, see: [compatibility](../about/compatibility.md#content-addressable-storage-cas)
|
||||
|
||||
## Detail
|
||||
|
||||
|
|
|
@ -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
|
||||
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.
|
||||
|
||||
## Refresh token format
|
||||
|
|
|
@ -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
|
||||
`sub` and `aud`. The resource scope is put into the `access` field. The
|
||||
structure of the access field can be seen in the
|
||||
[jwt documentation](../jwt).
|
||||
[jwt documentation](jwt.md).
|
||||
|
||||
## Refresh Tokens
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ keywords: registry, on-prem, images, tags, repository, distribution, Bearer auth
|
|||
|
||||
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.
|
||||
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
|
||||
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
|
||||
only supports Basic Authentication. If an attempt to authenticate to the token
|
||||
server fails, the token server should return a `401 Unauthorized` response
|
||||
|
|
|
@ -71,7 +71,7 @@ image manifest based on the Content-Type returned in the HTTP response.
|
|||
- **`digest`** *string*
|
||||
|
||||
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*
|
||||
|
||||
|
@ -187,7 +187,7 @@ image. It's the direct replacement for the schema-1 manifest.
|
|||
- **`digest`** *string*
|
||||
|
||||
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*
|
||||
|
||||
|
@ -213,7 +213,7 @@ image. It's the direct replacement for the schema-1 manifest.
|
|||
- **`digest`** *string*
|
||||
|
||||
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*
|
||||
|
||||
|
|
|
@ -20,7 +20,22 @@ An implementation of the `storagedriver.StorageDriver` interface which uses [Mic
|
|||
|
||||
## Related information
|
||||
|
||||
* To get information about
|
||||
[azure-blob-storage](https://azure.microsoft.com/en-us/services/storage/), visit
|
||||
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).
|
||||
* To get information about Azure blob storage [the offical docs](https://azure.microsoft.com/en-us/services/storage/).
|
||||
* 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).
|
||||
|
||||
## 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -17,4 +17,8 @@ An implementation of the `storagedriver.StorageDriver` interface which uses Goog
|
|||
|
||||
{{< hint type=note >}}
|
||||
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 >}}
|
||||
|
|
|
@ -7,7 +7,7 @@ title: In-memory storage driver (testing only)
|
|||
For purely tests purposes, you can use the `inmemory` storage driver. This
|
||||
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
|
||||
volatile memory, use the [`filesystem` driver](../filesystem) on a ramdisk.
|
||||
volatile memory, use the [`filesystem` driver](filesystem.md) on a ramdisk.
|
||||
|
||||
{{< 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.
|
||||
|
|
15
docs/content/storage-drivers/middleware/_index.md
Normal file
15
docs/content/storage-drivers/middleware/_index.md
Normal 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.
|
32
docs/content/storage-drivers/middleware/rewrite.md
Normal file
32
docs/content/storage-drivers/middleware/rewrite.md
Normal 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
|
||||
```
|
|
@ -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. |
|
||||
| `region` | yes | The AWS region in which your bucket exists. |
|
||||
| `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. |
|
||||
| `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`. |
|
||||
|
@ -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.
|
||||
|
||||
`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.
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
||||
[![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)
|
||||
[![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
|
||||
```
|
||||
|
||||
⚠️ 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.
|
||||
If you wish to mount the local filesystem to the `rootdirectory` of the
|
||||
`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
|
||||
|
@ -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
|
||||
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
baseURL: /
|
||||
baseURL: https://distribution.github.io/distribution
|
||||
languageCode: en-us
|
||||
title: CNCF Distribution
|
||||
theme: hugo-geekdoc
|
||||
|
@ -22,3 +22,7 @@ disablePathToLower: true
|
|||
params:
|
||||
geekdocRepo: "https://github.com/distribution/distribution"
|
||||
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
1
docs/i18n/en.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
footer_legal_notice: Trademarks
|
5
docs/layouts/_default/_markup/render-link.html
Normal file
5
docs/layouts/_default/_markup/render-link.html
Normal 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
134
go.mod
|
@ -5,8 +5,8 @@ go 1.21
|
|||
toolchain go1.21.4
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.30.1
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240402141532-e5040d35e99d
|
||||
cloud.google.com/go/storage v1.36.0
|
||||
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240716083621-e18b91623138
|
||||
git.frostfs.info/TrueCloudLab/tzhash v1.8.0
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24
|
||||
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/bshuster-repo/logrus-logstash-hook v1.0.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-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/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/hashicorp/golang-lru/arc/v2 v2.0.5
|
||||
github.com/klauspost/compress v1.17.4
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/nspcc-dev/neo-go v0.101.2-0.20230601131642-a0117042e8fc
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/nspcc-dev/neo-go v0.106.2
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
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/testcontainers/testcontainers-go v0.29.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/trace v1.22.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/oauth2 v0.11.0
|
||||
google.golang.org/api v0.126.0
|
||||
golang.org/x/net v0.26.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
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.7 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go v0.112.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/iam v1.1.1 // indirect
|
||||
cloud.google.com/go/iam v1.1.6 // 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-contract v0.0.0-20230307110621-19a8ef2d02fb // indirect
|
||||
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240530152826-2f6d3209e1d3 // 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/hrw v1.2.1 // 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/hcsshim v0.11.4 // 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/log v0.1.0 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.1 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/docker v25.0.3+incompatible // indirect
|
||||
github.com/docker/go-connections 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/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/protobuf v1.5.3 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.11.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.6.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // 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/magiconair/properties v1.8.7 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
|
@ -86,54 +106,31 @@ require (
|
|||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.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/neo-go/pkg/interop v0.0.0-20230615193820-9185820289ce // indirect
|
||||
github.com/nspcc-dev/rfc6979 v0.2.0 // 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-20240521091047-78685785716d // 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/pmezard/go-difflib v1.0.0 // 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/shirou/gopsutil/v3 v3.23.12 // 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/numcpus v0.6.1 // indirect
|
||||
github.com/twmb/murmur3 v1.1.8 // indirect
|
||||
github.com/urfave/cli v1.22.12 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.uber.org/atomic v1.10.0 // 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.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
|
||||
go.opentelemetry.io/otel v1.21.0
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.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/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/prometheus 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.21.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.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
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
go.uber.org/multierr v1.11.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/text v0.16.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/grpc v1.59.0
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240205150955-31a09d347014 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -72,7 +72,7 @@ type Manager interface {
|
|||
|
||||
// AddResponse adds the response to the challenge
|
||||
// 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
|
||||
// response was authorized, any challenges for the
|
||||
// endpoint will be cleared.
|
||||
|
|
|
@ -29,9 +29,9 @@ var (
|
|||
const defaultClientID = "registry-client"
|
||||
|
||||
// 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 {
|
||||
// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
|
||||
// Scheme returns the scheme as expected from the "WWW-Authenticate" header.
|
||||
Scheme() string
|
||||
|
||||
// AuthorizeRequest adds the authorization header to a request (if needed)
|
||||
|
|
|
@ -46,8 +46,14 @@ func parseHTTPErrorResponse(resp *http.Response) error {
|
|||
}
|
||||
|
||||
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 == "" {
|
||||
return makeError(statusCode, string(body))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
json := `{"errors":[{"code":"DIGEST_INVALID","message":"provided digest does not match"}]}`
|
||||
response := &http.Response{
|
||||
|
|
|
@ -26,11 +26,6 @@ var (
|
|||
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
|
||||
// request. When seeking and starting a read from a non-zero offset
|
||||
// the a "Range" header will be added which sets the offset.
|
||||
|
|
|
@ -47,7 +47,7 @@ type ManifestBuilder interface {
|
|||
AppendReference(dependency Describable) error
|
||||
}
|
||||
|
||||
// ManifestService describes operations on image manifests.
|
||||
// ManifestService describes operations on manifests.
|
||||
type ManifestService interface {
|
||||
// Exists returns true if the manifest exists.
|
||||
Exists(ctx context.Context, dgst digest.Digest) (bool, error)
|
||||
|
|
|
@ -269,7 +269,7 @@ type RouteDescriptor struct {
|
|||
// should match.
|
||||
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.
|
||||
Entity string
|
||||
|
||||
|
|
|
@ -202,7 +202,7 @@ func (ub *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string,
|
|||
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.
|
||||
func (ub *URLBuilder) cloneRoute(name string) clonedRoute {
|
||||
route := new(mux.Route)
|
||||
|
|
|
@ -46,7 +46,7 @@ var (
|
|||
)
|
||||
|
||||
// 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)
|
||||
|
||||
var accessControllers map[string]InitFunc
|
||||
|
@ -56,7 +56,7 @@ func init() {
|
|||
}
|
||||
|
||||
// UserInfo carries information about
|
||||
// an autenticated/authorized client.
|
||||
// an authenticated/authorized client.
|
||||
type UserInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -86,6 +87,7 @@ type authChallenge struct {
|
|||
err error
|
||||
realm string
|
||||
autoRedirect bool
|
||||
autoRedirectPath string
|
||||
service string
|
||||
accessSet accessSet
|
||||
}
|
||||
|
@ -102,13 +104,28 @@ func (ac authChallenge) Status() int {
|
|||
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
|
||||
// the WWW-Authenticate response challenge header.
|
||||
// See https://tools.ietf.org/html/rfc6750#section-3
|
||||
func (ac authChallenge) challengeParams(r *http.Request) string {
|
||||
var realm string
|
||||
if ac.autoRedirect {
|
||||
realm = fmt.Sprintf("https://%s/auth/token", r.Host)
|
||||
realm = buildAutoRedirectURL(r, ac.autoRedirectPath)
|
||||
} else {
|
||||
realm = ac.realm
|
||||
}
|
||||
|
@ -127,7 +144,7 @@ func (ac authChallenge) challengeParams(r *http.Request) string {
|
|||
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) {
|
||||
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 {
|
||||
realm string
|
||||
autoRedirect bool
|
||||
autoRedirectPath string
|
||||
issuer string
|
||||
service string
|
||||
rootCerts *x509.CertPool
|
||||
trustedKeys map[string]crypto.PublicKey
|
||||
signingAlgorithms []jose.SignatureAlgorithm
|
||||
}
|
||||
|
||||
const (
|
||||
defaultAutoRedirectPath = "/auth/token"
|
||||
)
|
||||
|
||||
// tokenAccessOptions is a convenience type for handling
|
||||
// options to the contstructor of an accessController.
|
||||
// options to the constructor of an accessController.
|
||||
type tokenAccessOptions struct {
|
||||
realm string
|
||||
autoRedirect bool
|
||||
autoRedirectPath string
|
||||
issuer string
|
||||
service string
|
||||
rootCertBundle string
|
||||
jwks string
|
||||
signingAlgorithms []string
|
||||
}
|
||||
|
||||
// checkOptions gathers the necessary options
|
||||
|
@ -183,10 +208,32 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
|
|||
if ok {
|
||||
autoRedirect, ok := autoRedirectVal.(bool)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -243,6 +290,18 @@ func getJwks(path string) (*jose.JSONWebKeySet, error) {
|
|||
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.
|
||||
func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
|
||||
config, err := checkOptions(options)
|
||||
|
@ -253,6 +312,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
|
|||
var (
|
||||
rootCerts []*x509.Certificate
|
||||
jwks *jose.JSONWebKeySet
|
||||
signAlgos []jose.SignatureAlgorithm
|
||||
)
|
||||
|
||||
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{
|
||||
realm: config.realm,
|
||||
autoRedirect: config.autoRedirect,
|
||||
autoRedirectPath: config.autoRedirectPath,
|
||||
issuer: config.issuer,
|
||||
service: config.service,
|
||||
rootCerts: rootPool,
|
||||
trustedKeys: trustedKeys,
|
||||
signingAlgorithms: signAlgos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -302,6 +374,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
|
|||
challenge := &authChallenge{
|
||||
realm: ac.realm,
|
||||
autoRedirect: ac.autoRedirect,
|
||||
autoRedirectPath: ac.autoRedirectPath,
|
||||
service: ac.service,
|
||||
accessSet: newAccessSet(accessItems...),
|
||||
}
|
||||
|
@ -312,7 +385,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
|
|||
return nil, challenge
|
||||
}
|
||||
|
||||
token, err := NewToken(rawToken)
|
||||
token, err := NewToken(rawToken, ac.signingAlgorithms)
|
||||
if err != nil {
|
||||
challenge.err = err
|
||||
return nil, challenge
|
||||
|
|
89
registry/auth/token/accesscontroller_test.go
Normal file
89
registry/auth/token/accesscontroller_test.go
Normal 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")
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
fuzz "github.com/AdaLogics/go-fuzz-headers"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
)
|
||||
|
||||
func FuzzToken1(f *testing.F) {
|
||||
|
@ -18,7 +19,7 @@ func FuzzToken1(f *testing.F) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
token, err := NewToken(rawToken)
|
||||
token, err := NewToken(rawToken, []jose.SignatureAlgorithm{jose.EdDSA, jose.RS384})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/distribution/distribution/v3/registry/auth"
|
||||
|
@ -23,6 +23,38 @@ const (
|
|||
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.
|
||||
var (
|
||||
ErrMalformedToken = errors.New("malformed token")
|
||||
|
@ -69,8 +101,8 @@ type VerifyOptions struct {
|
|||
|
||||
// NewToken parses the given raw token string
|
||||
// and constructs an unverified JSON Web Token.
|
||||
func NewToken(rawToken string) (*Token, error) {
|
||||
token, err := jwt.ParseSigned(rawToken)
|
||||
func NewToken(rawToken string, signingAlgs []jose.SignatureAlgorithm) (*Token, error) {
|
||||
token, err := jwt.ParseSigned(rawToken, signingAlgs)
|
||||
if err != nil {
|
||||
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.
|
||||
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 {
|
||||
case header.JSONWebKey != nil:
|
||||
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)
|
||||
}
|
||||
default:
|
||||
signingKey, err = verifyCertChain(header, verifyOpts.Roots)
|
||||
err = ErrInvalidToken
|
||||
}
|
||||
|
||||
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
|
||||
// *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)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/distribution/distribution/v3/registry/auth"
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
)
|
||||
|
||||
func makeRootKeys(numKeys int) ([]*ecdsa.PrivateKey, error) {
|
||||
|
@ -123,12 +123,12 @@ func makeTestToken(jwk *jose.JSONWebKey, issuer, audience string, access []*Reso
|
|||
Access: access,
|
||||
}
|
||||
|
||||
tokenString, err := jwt.Signed(signer).Claims(claimSet).CompactSerialize()
|
||||
tokenString, err := jwt.Signed(signer).Claims(claimSet).Serialize()
|
||||
if err != nil {
|
||||
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
|
||||
|
|
|
@ -1709,6 +1709,33 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.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)
|
||||
if err != nil {
|
||||
t.Fatalf("Error constructing request: %s", err)
|
||||
|
@ -1744,6 +1771,32 @@ func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Name
|
|||
|
||||
// ---------------
|
||||
// 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)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
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
|
||||
// suffice as a match.
|
||||
func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) {
|
||||
t.Helper()
|
||||
|
||||
for k, vs := range headers {
|
||||
if resp.Header.Get(k) == "" {
|
||||
t.Fatalf("response missing header %q", k)
|
||||
|
|
|
@ -3,6 +3,8 @@ package handlers
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"math"
|
||||
|
@ -77,7 +79,7 @@ type App struct {
|
|||
source notifications.SourceRecord
|
||||
}
|
||||
|
||||
redis *redis.Client
|
||||
redis redis.UniversalClient
|
||||
|
||||
// isCache is true if this registry is configured as a pull through cache
|
||||
isCache bool
|
||||
|
@ -114,7 +116,7 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
|
|||
storageParams = make(configuration.Parameters)
|
||||
}
|
||||
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
|
||||
|
@ -155,7 +157,11 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App {
|
|||
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.configureEvents(config)
|
||||
app.configureRedis(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
|
||||
var redirectDisabled bool
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
// passed through the application filters and context will be constructed at
|
||||
// request time.
|
||||
|
@ -487,12 +531,41 @@ func (app *App) configureEvents(configuration *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")
|
||||
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.
|
||||
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 {
|
||||
return redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Addr,
|
||||
OnConnect: func(ctx context.Context, cn *redis.Conn) error {
|
||||
func (app *App) createPool(cfg redis.UniversalOptions) redis.UniversalClient {
|
||||
cfg.OnConnect = func(ctx context.Context, cn *redis.Conn) error {
|
||||
res := cn.Ping(ctx)
|
||||
return res.Err()
|
||||
},
|
||||
Username: cfg.Username,
|
||||
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,
|
||||
})
|
||||
}
|
||||
return redis.NewUniversalClient(&cfg)
|
||||
}
|
||||
|
||||
// configureLogHook prepares logging hook parameters.
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
"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/schema2"
|
||||
"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/reference"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/opencontainers/go-digest"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
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("Docker-Content-Digest", imh.Digest.String())
|
||||
w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest))
|
||||
|
||||
if r.Method == http.MethodHead {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := w.Write(p); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
@ -476,12 +484,26 @@ func (imh *manifestHandler) DeleteManifest(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
var (
|
||||
errs []error
|
||||
mu sync.Mutex
|
||||
)
|
||||
g := errgroup.Group{}
|
||||
g.SetLimit(storage.DefaultConcurrencyLimit)
|
||||
for _, tag := range referencedTags {
|
||||
tag := tag
|
||||
|
||||
g.Go(func() error {
|
||||
if err := tagService.Untag(imh, tag); err != nil {
|
||||
imh.Errors = append(imh.Errors, err)
|
||||
return
|
||||
mu.Lock()
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -17,14 +17,23 @@ type userpass struct {
|
|||
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 {
|
||||
creds map[string]userpass
|
||||
}
|
||||
|
||||
func (c credentials) Basic(u *url.URL) (string, string) {
|
||||
up := c.creds[u.String()]
|
||||
|
||||
return up.username, up.password
|
||||
return c.creds[u.String()].Basic(u)
|
||||
}
|
||||
|
||||
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
|
||||
func configureAuth(username, password, remoteURL string) (auth.CredentialStore, error) {
|
||||
func configureAuth(username, password, remoteURL string) (auth.CredentialStore, auth.CredentialStore, error) {
|
||||
creds := map[string]userpass{}
|
||||
|
||||
authURLs, err := getAuthURLs(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -33,22 +33,20 @@ var inflight = make(map[digest.Digest]struct{})
|
|||
// mu protects inflight
|
||||
var mu sync.Mutex
|
||||
|
||||
func setResponseHeaders(w http.ResponseWriter, length int64, mediaType string, digest digest.Digest) {
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(length, 10))
|
||||
w.Header().Set("Content-Type", mediaType)
|
||||
w.Header().Set("Docker-Content-Digest", digest.String())
|
||||
w.Header().Set("Etag", digest.String())
|
||||
func setResponseHeaders(h http.Header, length int64, mediaType string, digest digest.Digest) {
|
||||
h.Set("Content-Length", strconv.FormatInt(length, 10))
|
||||
h.Set("Content-Type", mediaType)
|
||||
h.Set("Docker-Content-Digest", 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)
|
||||
if err != nil {
|
||||
return distribution.Descriptor{}, err
|
||||
}
|
||||
|
||||
if w, ok := writer.(http.ResponseWriter); ok {
|
||||
setResponseHeaders(w, desc.Size, desc.MediaType, dgst)
|
||||
}
|
||||
setResponseHeaders(h, desc.Size, desc.MediaType, dgst)
|
||||
|
||||
remoteReader, err := pbs.remoteStore.Open(ctx, dgst)
|
||||
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.
|
||||
// TODO Maybe we could reuse the these blobs are serving remotely and caching locally.
|
||||
mu.Unlock()
|
||||
_, err := pbs.copyContent(ctx, dgst, w)
|
||||
_, err := pbs.copyContent(ctx, dgst, w, w.Header())
|
||||
return err
|
||||
}
|
||||
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.
|
||||
// This can prevent a redundant blob fetching.
|
||||
multiWriter := io.MultiWriter(w, bw)
|
||||
desc, err := pbs.copyContent(ctx, dgst, multiWriter)
|
||||
desc, err := pbs.copyContent(ctx, dgst, multiWriter, w.Header())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -448,12 +448,22 @@ func testProxyStoreServe(t *testing.T, te *testEnv, numClients int) {
|
|||
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)
|
||||
if localDigest != remoteBlob.Digest {
|
||||
t.Errorf("Mismatching blob fetch from proxy")
|
||||
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)
|
||||
if err != nil {
|
||||
|
|
|
@ -62,6 +62,16 @@ func init() {
|
|||
}))
|
||||
|
||||
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
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
"github.com/distribution/distribution/v3/configuration"
|
||||
"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/storage"
|
||||
"github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
"github.com/distribution/reference"
|
||||
)
|
||||
|
||||
var repositoryTTL = 24 * 7 * time.Hour
|
||||
|
@ -30,6 +31,7 @@ type proxyingRegistry struct {
|
|||
ttl *time.Duration
|
||||
remoteURL url.URL
|
||||
authChallenger authChallenger
|
||||
basicAuth auth.CredentialStore
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -127,6 +129,7 @@ func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Name
|
|||
cm: challenge.NewSimpleManager(),
|
||||
cs: cs,
|
||||
},
|
||||
basicAuth: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -155,7 +158,8 @@ func (pr *proxyingRegistry) Repository(ctx context.Context, name reference.Named
|
|||
|
||||
tr := transport.NewTransport(http.DefaultTransport,
|
||||
auth.NewAuthorizer(c.challengeManager(),
|
||||
auth.NewTokenHandlerWithOptions(tkopts)))
|
||||
auth.NewTokenHandlerWithOptions(tkopts),
|
||||
auth.NewBasicHandler(pr.basicAuth)))
|
||||
|
||||
localRepo, err := pr.embedded.Repository(ctx, name)
|
||||
if err != nil {
|
||||
|
@ -211,6 +215,15 @@ func (pr *proxyingRegistry) BlobStatter() distribution.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
|
||||
type authChallenger interface {
|
||||
tryEstablishChallenges(context.Context) error
|
||||
|
|
|
@ -206,12 +206,13 @@ func (ttles *TTLExpirationScheduler) startTimer(entry *schedulerEntry, ttl time.
|
|||
}
|
||||
|
||||
// Stop stops the scheduler.
|
||||
func (ttles *TTLExpirationScheduler) Stop() {
|
||||
func (ttles *TTLExpirationScheduler) Stop() error {
|
||||
ttles.Lock()
|
||||
defer ttles.Unlock()
|
||||
|
||||
if err := ttles.writeState(); err != nil {
|
||||
dcontext.GetLogger(ttles.ctx).Errorf("Error writing scheduler state: %s", err)
|
||||
err := ttles.writeState()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error writing scheduler state: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range ttles.entries {
|
||||
|
@ -221,6 +222,7 @@ func (ttles *TTLExpirationScheduler) Stop() {
|
|||
close(ttles.doneChan)
|
||||
ttles.saveTimer.Stop()
|
||||
ttles.stopped = true
|
||||
return err
|
||||
}
|
||||
|
||||
func (ttles *TTLExpirationScheduler) writeState() error {
|
||||
|
|
|
@ -136,7 +136,12 @@ func TestRestoreOld(t *testing.T) {
|
|||
if err != nil {
|
||||
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()
|
||||
mu.Lock()
|
||||
|
@ -177,7 +182,10 @@ func TestStopRestore(t *testing.T) {
|
|||
|
||||
// Start and stop before all operations complete
|
||||
// state will be written to fs
|
||||
s.Stop()
|
||||
err = s.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// v2 will restore state from fs
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -20,6 +21,8 @@ import (
|
|||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"golang.org/x/crypto/acme"
|
||||
"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/health"
|
||||
|
@ -79,9 +82,6 @@ var tlsVersions = map[string]uint16{
|
|||
// defaultLogFormatter is the default formatter to use for logs.
|
||||
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
|
||||
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.",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// setup context
|
||||
ctx := dcontext.WithVersion(dcontext.Background(), version.Version)
|
||||
ctx := dcontext.WithVersion(dcontext.Background(), version.Version())
|
||||
|
||||
config, err := resolveConfiguration(args)
|
||||
if err != nil {
|
||||
|
@ -128,6 +128,7 @@ type Registry struct {
|
|||
config *configuration.Configuration
|
||||
app *handlers.App
|
||||
server *http.Server
|
||||
quit chan os.Signal
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
|
||||
server := &http.Server{
|
||||
|
@ -168,6 +172,7 @@ func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Reg
|
|||
app: app,
|
||||
config: config,
|
||||
server: server,
|
||||
quit: make(chan os.Signal, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -308,7 +313,7 @@ func (registry *Registry) ListenAndServe() error {
|
|||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Start serving in goroutine and listen for stop signal in main thread
|
||||
|
@ -319,15 +324,24 @@ func (registry *Registry) ListenAndServe() error {
|
|||
select {
|
||||
case err := <-serveErr:
|
||||
return err
|
||||
case <-quit:
|
||||
case <-registry.quit:
|
||||
dcontext.GetLogger(registry.app).Info("stopping server gracefully. Draining connections for ", config.HTTP.DrainTimeout)
|
||||
// shutdown the server with a grace period of configured timeout
|
||||
c, cancel := context.WithTimeout(context.Background(), config.HTTP.DrainTimeout)
|
||||
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) {
|
||||
if config.HTTP.Debug.Addr != "" {
|
||||
go func(addr string) {
|
||||
|
|
|
@ -103,7 +103,7 @@ func TestGracefulShutdown(t *testing.T) {
|
|||
fmt.Fprintf(conn, "GET /v2/ ")
|
||||
|
||||
// send stop signal
|
||||
quit <- os.Interrupt
|
||||
registry.quit <- os.Interrupt
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// try connecting again. it shouldn't
|
||||
|
@ -325,7 +325,7 @@ func TestRegistrySupportedCipherSuite(t *testing.T) {
|
|||
}
|
||||
|
||||
// send stop signal
|
||||
quit <- os.Interrupt
|
||||
registry.quit <- os.Interrupt
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
|
@ -369,7 +369,7 @@ func TestRegistryUnsupportedCipherSuite(t *testing.T) {
|
|||
}
|
||||
|
||||
// send stop signal
|
||||
quit <- os.Interrupt
|
||||
registry.quit <- os.Interrupt
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ func TestWriteSeek(t *testing.T) {
|
|||
}
|
||||
contents := []byte{1, 2, 3}
|
||||
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()
|
||||
offset := blobUpload.Size()
|
||||
|
|
|
@ -230,7 +230,7 @@ func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descri
|
|||
}
|
||||
|
||||
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
|
||||
// because we've written the entire file in the lifecycle of the
|
||||
// current instance.
|
||||
|
|
4
registry/storage/cache/redis/redis.go
vendored
4
registry/storage/cache/redis/redis.go
vendored
|
@ -25,7 +25,7 @@ import (
|
|||
// 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.
|
||||
type redisBlobDescriptorService struct {
|
||||
pool *redis.Client
|
||||
pool redis.UniversalClient
|
||||
|
||||
// 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
|
||||
|
@ -37,7 +37,7 @@ var _ distribution.BlobDescriptorService = &redisBlobDescriptorService{}
|
|||
|
||||
// NewRedisBlobDescriptorCacheProvider returns a new redis-based
|
||||
// BlobDescriptorCacheProvider using the provided redis connection pool.
|
||||
func NewRedisBlobDescriptorCacheProvider(pool *redis.Client) cache.BlobDescriptorCacheProvider {
|
||||
func NewRedisBlobDescriptorCacheProvider(pool redis.UniversalClient) cache.BlobDescriptorCacheProvider {
|
||||
return metrics.NewPrometheusCacheProvider(
|
||||
&redisBlobDescriptorService{
|
||||
pool: pool,
|
||||
|
|
2
registry/storage/cache/redis/redis_test.go
vendored
2
registry/storage/cache/redis/redis_test.go
vendored
|
@ -20,7 +20,7 @@ func init() {
|
|||
// implementation.
|
||||
func TestRedisBlobDescriptorCacheProvider(t *testing.T) {
|
||||
if redisAddr == "" {
|
||||
// fallback to an environement variable
|
||||
// fallback to an environment variable
|
||||
redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR")
|
||||
}
|
||||
|
||||
|
|
|
@ -46,12 +46,20 @@ import (
|
|||
"github.com/distribution/distribution/v3/internal/dcontext"
|
||||
prometheus "github.com/distribution/distribution/v3/metrics"
|
||||
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
"github.com/distribution/distribution/v3/tracing"
|
||||
"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
|
||||
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() {
|
||||
metrics.Register(prometheus.StorageNamespace)
|
||||
}
|
||||
|
@ -89,8 +97,16 @@ func (base *Base) setDriverName(e error) error {
|
|||
|
||||
// GetContent wraps GetContent of underlying storage driver.
|
||||
func (base *Base) GetContent(ctx context.Context, path string) ([]byte, error) {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.GetContent(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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) {
|
||||
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.
|
||||
func (base *Base) PutContent(ctx context.Context, path string, content []byte) error {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.PutContent(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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) {
|
||||
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.
|
||||
func (base *Base) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.Reader(%q, %d)", base.Name(), path, offset)
|
||||
attrs := []attribute.KeyValue{
|
||||
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 {
|
||||
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.
|
||||
func (base *Base) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.Writer(%q, %v)", base.Name(), path, append)
|
||||
attrs := []attribute.KeyValue{
|
||||
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) {
|
||||
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.
|
||||
func (base *Base) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.Stat(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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 != "/" {
|
||||
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.
|
||||
func (base *Base) List(ctx context.Context, path string) ([]string, error) {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.List(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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 != "/" {
|
||||
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.
|
||||
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)
|
||||
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.
|
||||
func (base *Base) Delete(ctx context.Context, path string) error {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.Delete(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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) {
|
||||
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.
|
||||
func (base *Base) RedirectURL(r *http.Request, path string) (string, error) {
|
||||
ctx, done := dcontext.WithTrace(r.Context())
|
||||
defer done("%s.RedirectURL(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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) {
|
||||
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.
|
||||
func (base *Base) Walk(ctx context.Context, path string, f storagedriver.WalkFn, options ...func(*storagedriver.WalkOptions)) error {
|
||||
ctx, done := dcontext.WithTrace(ctx)
|
||||
defer done("%s.Walk(%q)", base.Name(), path)
|
||||
attrs := []attribute.KeyValue{
|
||||
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 != "/" {
|
||||
return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()}
|
||||
|
|
|
@ -155,6 +155,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
|
|||
jwtConf := new(jwt.Config)
|
||||
var err error
|
||||
var gcs *storage.Client
|
||||
var options []option.ClientOption
|
||||
if keyfile, ok := parameters["keyfile"]; ok {
|
||||
jsonKey, err := os.ReadFile(fmt.Sprint(keyfile))
|
||||
if err != nil {
|
||||
|
@ -165,10 +166,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
|
|||
return nil, err
|
||||
}
|
||||
ts = jwtConf.TokenSource(ctx)
|
||||
gcs, err = storage.NewClient(ctx, option.WithCredentialsFile(fmt.Sprint(keyfile)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options = append(options, option.WithCredentialsFile(fmt.Sprint(keyfile)))
|
||||
} else if credentials, ok := parameters["credentials"]; ok {
|
||||
credentialMap, ok := credentials.(map[interface{}]interface{})
|
||||
if !ok {
|
||||
|
@ -194,10 +192,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (sto
|
|||
return nil, err
|
||||
}
|
||||
ts = jwtConf.TokenSource(ctx)
|
||||
gcs, err = storage.NewClient(ctx, option.WithCredentialsJSON(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options = append(options, option.WithCredentialsJSON(data))
|
||||
} else {
|
||||
var err error
|
||||
// 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 {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
maxConcurrency, err := base.GetLimitFromParameter(parameters["maxconcurrency"], minConcurrency, defaultMaxConcurrency)
|
||||
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
|
||||
// the given path, possibly using the given options.
|
||||
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 {
|
||||
return "", nil
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
gcsDriverConstructor = func(rootDirectory string) (storagedriver.StorageDriver, error) {
|
||||
jsonKey, err := os.ReadFile(credentials)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error reading JSON key : %v", err))
|
||||
|
@ -67,7 +68,6 @@ func init() {
|
|||
panic(fmt.Sprintf("Error initializing gcs client : %v", err))
|
||||
}
|
||||
|
||||
gcsDriverConstructor = func(rootDirectory string) (storagedriver.StorageDriver, error) {
|
||||
parameters := driverParameters{
|
||||
bucket: bucket,
|
||||
rootDirectory: rootDirectory,
|
||||
|
|
|
@ -50,6 +50,6 @@ pZeMRablbPQdp8/1NyIwimq1VlG0ohQ4P6qhW7E09ZMC
|
|||
t.Fatal(err)
|
||||
}
|
||||
if storageDriver == nil {
|
||||
t.Fatal("Driver couldnt be initialized.")
|
||||
t.Fatal("Driver could not be initialized")
|
||||
}
|
||||
}
|
||||
|
|
86
registry/storage/driver/middleware/rewrite/middleware.go
Normal file
86
registry/storage/driver/middleware/rewrite/middleware.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -205,7 +205,7 @@ func FromParameters(ctx context.Context, parameters map[string]interface{}) (*Dr
|
|||
regionEndpoint = ""
|
||||
}
|
||||
|
||||
forcePathStyleBool := true
|
||||
forcePathStyleBool := false
|
||||
forcePathStyle := parameters["forcepathstyle"]
|
||||
switch forcePathStyle := forcePathStyle.(type) {
|
||||
case string:
|
||||
|
@ -529,9 +529,9 @@ func New(ctx context.Context, params DriverParameters) (*Driver, error) {
|
|||
|
||||
if params.RegionEndpoint != "" {
|
||||
awsConfig.WithEndpoint(params.RegionEndpoint)
|
||||
awsConfig.WithS3ForcePathStyle(params.ForcePathStyle)
|
||||
}
|
||||
|
||||
awsConfig.WithS3ForcePathStyle(params.ForcePathStyle)
|
||||
awsConfig.WithS3UseAccelerate(params.Accelerate)
|
||||
awsConfig.WithRegion(params.Region)
|
||||
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
|
||||
dirs := directoryDiff(prevDir, filePath)
|
||||
if len(dirs) > 0 {
|
||||
for _, dir := range dirs {
|
||||
walkInfos = append(walkInfos, storagedriver.FileInfoInternal{
|
||||
FileInfoFields: storagedriver.FileInfoFields{
|
||||
|
@ -1138,7 +1137,6 @@ func (d *driver) doWalk(parentCtx context.Context, objectCount *int64, from stri
|
|||
})
|
||||
prevDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
walkInfos = append(walkInfos, storagedriver.FileInfoInternal{
|
||||
FileInfoFields: storagedriver.FileInfoFields{
|
||||
|
|
|
@ -80,7 +80,7 @@ func init() {
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
forcePathStyleBool := true
|
||||
forcePathStyleBool := false
|
||||
if forcePathStyle != "" {
|
||||
forcePathStyleBool, err = strconv.ParseBool(forcePathStyle)
|
||||
if err != nil {
|
||||
|
@ -203,6 +203,8 @@ func TestEmptyRootList(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClientTransport(t *testing.T) {
|
||||
skipCheck(t)
|
||||
|
||||
testCases := []struct {
|
||||
skipverify bool
|
||||
}{
|
||||
|
@ -243,7 +245,7 @@ func TestClientTransport(t *testing.T) {
|
|||
return
|
||||
}
|
||||
// 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 {
|
||||
t.Errorf("unexpected S3 driver client transport")
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package storage
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/distribution/distribution/v3"
|
||||
|
@ -36,6 +37,7 @@ func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, regis
|
|||
|
||||
// mark
|
||||
markSet := make(map[digest.Digest]struct{})
|
||||
deleteLayerSet := make(map[string][]digest.Digest)
|
||||
manifestArr := make([]ManifestDel, 0)
|
||||
err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error {
|
||||
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)
|
||||
}
|
||||
if len(tags) == 0 {
|
||||
emit("manifest eligible for deletion: %s", dgst)
|
||||
// fetch all tags from repository
|
||||
// all of these tags could contain manifest in history
|
||||
// which means that we need check (and delete) those references when deleting manifest
|
||||
allTags, err := repository.Tags(ctx).All(ctx)
|
||||
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)
|
||||
}
|
||||
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)
|
||||
markSet[dgst] = struct{}{}
|
||||
|
||||
manifest, err := manifestService.Get(ctx, dgst)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err)
|
||||
return markManifestReferences(dgst, manifestService, ctx, func(d digest.Digest) bool {
|
||||
_, marked := markSet[d]
|
||||
if !marked {
|
||||
markSet[d] = struct{}{}
|
||||
emit("%s: marking blob %s", repoName, d)
|
||||
}
|
||||
|
||||
descriptors := manifest.References()
|
||||
for _, descriptor := range descriptors {
|
||||
markSet[descriptor.Digest] = struct{}{}
|
||||
emit("%s: marking blob %s", repoName, descriptor.Digest)
|
||||
}
|
||||
|
||||
return nil
|
||||
return marked
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// In certain situations such as unfinished uploads, deleting all
|
||||
// tags in S3 or removing the _manifests folder manually, this
|
||||
// error may be of type PathNotFound.
|
||||
//
|
||||
// In these cases we can continue marking other manifests safely.
|
||||
if _, ok := err.(driver.PathNotFoundError); ok {
|
||||
return nil
|
||||
if _, ok := err.(driver.PathNotFoundError); !ok {
|
||||
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
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark: %v", err)
|
||||
}
|
||||
|
||||
manifestArr = unmarkReferencedManifest(manifestArr, markSet)
|
||||
|
||||
// sweep
|
||||
vacuum := NewVacuum(ctx, storageDriver)
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
|
||||
"github.com/distribution/distribution/v3"
|
||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
||||
"github.com/distribution/distribution/v3/manifest/ocischema"
|
||||
"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/testutil"
|
||||
"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) {
|
||||
ctx := dcontext.Background()
|
||||
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) {
|
||||
ctx := dcontext.Background()
|
||||
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
Loading…
Reference in a new issue