diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:48:08 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:48:08 +0000 |
commit | b65b89d538e8c6adad31b84584fe2c53ba8ebc09 (patch) | |
tree | 6fe7ff2b7c36ddf98d24c8a854ca6299103658d1 | |
parent | Initial commit. (diff) | |
download | go-containerregistry-upstream.tar.xz go-containerregistry-upstream.zip |
Adding upstream version 0.14.0+ds1.upstream/0.14.0+ds1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
474 files changed, 65397 insertions, 0 deletions
diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 0000000..68c99ae --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,2 @@ +ignore: + - "**/zz_*_generated.go" # Ignore generated files. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e4fb951 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# This file is documented at https://git-scm.com/docs/gitattributes. +# Linguist-specific attributes are documented at +# https://github.com/github/linguist. + +**/zz_deepcopy_generated.go linguist-generated=true +cmd/crane/doc/crane*.md linguist-generated=true +go.sum linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/crane_bug_report.md b/.github/ISSUE_TEMPLATE/crane_bug_report.md new file mode 100644 index 0000000..fb14c38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crane_bug_report.md @@ -0,0 +1,25 @@ +--- +name: crane bug report +about: Create a report to help us improve the crane or gcrane CLIs +title: 'crane:' +labels: bug +assignees: '' + +--- + +### Describe the bug + +A clear and concise description of what the bug is. + +### To Reproduce + +### Expected behavior + +A clear and concise description of what you expected to happen. + +### Additional context + +Add any other context about the problem here. + +- Output of `crane version` +- Registry used (e.g., GCR, ECR, Quay) diff --git a/.github/ISSUE_TEMPLATE/ggcr_bug_report.md b/.github/ISSUE_TEMPLATE/ggcr_bug_report.md new file mode 100644 index 0000000..790d97a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ggcr_bug_report.md @@ -0,0 +1,25 @@ +--- +name: Go library bug report +about: Create a report to help us improve the Go library +title: 'ggcr:' +labels: bug +assignees: '' + +--- + +### Describe the bug + +A clear and concise description of what the bug is. + +### To Reproduce + +### Expected behavior + +A clear and concise description of what you expected to happen. + +### Additional context + +Add any other context about the problem here. + +- Version of the module +- Registry used (e.g., GCR, ECR, Quay) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..ff4f551 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,9 @@ +--- +name: Question +about: Ask a question about the project +title: 'question:' +labels: question +assignees: '' + +--- + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e2347a8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/analyze.yaml b/.github/workflows/analyze.yaml new file mode 100644 index 0000000..5982e60 --- /dev/null +++ b/.github/workflows/analyze.yaml @@ -0,0 +1,25 @@ +name: Analyze + +on: + workflow_dispatch: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + - uses: github/codeql-action/init@v2 + with: + languages: go + - uses: github/codeql-action/autobuild@v2 + - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/boilerplate.yaml b/.github/workflows/boilerplate.yaml new file mode 100644 index 0000000..3782e51 --- /dev/null +++ b/.github/workflows/boilerplate.yaml @@ -0,0 +1,33 @@ +name: Boilerplate + +on: + pull_request: + branches: ['main'] + +jobs: + + check: + name: Boilerplate Check + runs-on: ubuntu-latest + strategy: + fail-fast: false # Keep running if one leg fails. + matrix: + extension: + - go + - sh + + # Map between extension and human-readable name. + include: + - extension: go + language: Go + - extension: sh + language: Bash + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - uses: chainguard-dev/actions/boilerplate@5e21cb47971231c078a677dfe89a348371cb880c # main + with: + extension: ${{ matrix.extension }} + language: ${{ matrix.language }} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..b3ba675 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,26 @@ +name: Build + +on: + pull_request: + branches: ['main'] + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + + strategy: + matrix: + go-version: [1.19, '1.20'] + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + + - run: | + go build ./... + go test -run=^$ ./... diff --git a/.github/workflows/bump-deps.yaml b/.github/workflows/bump-deps.yaml new file mode 100644 index 0000000..4712489 --- /dev/null +++ b/.github/workflows/bump-deps.yaml @@ -0,0 +1,35 @@ +name: Bump Deps + +on: + schedule: + - cron: '0 6 * * 2' # weekly at 6AM Tuesday + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + bump-deps: + name: Bump Deps + + # Don't bother bumping deps on forks. + if: ${{ github.repository == 'google/go-containerregistry' }} + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - run: ./hack/bump-deps.sh + - name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + title: "Bump dependencies using hack/bump-deps.sh" + commit-message: "Bump dependencies using hack/bump-deps.sh" + labels: dependencies + assignees: imjasonh + delete-branch: true diff --git a/.github/workflows/donotsubmit.yaml b/.github/workflows/donotsubmit.yaml new file mode 100644 index 0000000..92d454b --- /dev/null +++ b/.github/workflows/donotsubmit.yaml @@ -0,0 +1,15 @@ +name: Do Not Submit + +on: + pull_request: + branches: ['main'] + +jobs: + + donotsubmit: + name: Do Not Submit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: chainguard-dev/actions/donotsubmit@5e21cb47971231c078a677dfe89a348371cb880c # main diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 0000000..0991bf7 --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,94 @@ +name: Basic e2e test + +on: + pull_request: + branches: ['main'] + +jobs: + e2e: + strategy: + fail-fast: false + matrix: + platform: + - ubuntu-latest + - windows-latest + name: e2e ${{ matrix.platform }} + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - name: crane append to an image, set the entrypoint, run it locally, roundtrip it + shell: bash + run: | + set -euxo pipefail + + # Setup local registry + go run ./cmd/registry & + + base=alpine + platform=linux/amd64 + if [[ "${{ matrix.platform }}" == "windows-latest" ]]; then + base=mcr.microsoft.com/windows/nanoserver:ltsc2022 + platform=windows/amd64 + fi + + CGO_ENABLED=0 go build -o app/crane ./cmd/crane + tar cvf crane.tar app + + # This prevents Bash for Windows from mangling path names. + # It shouldn't be necessary in general unless you're using Bash for + # Windows. + export MSYS_NO_PATHCONV=1 + + img=$(./app/crane mutate \ + --entrypoint=/app/crane,version \ + $(./app/crane append \ + --platform ${platform} \ + --base ${base} \ + --new_tag localhost:1338/append-test \ + --new_layer crane.tar)) + + # Run the image with and without args. + docker run $img + docker run $img --help + + # Make sure we can roundtrip it through pull/push + layout=$(mktemp -d) + dst=localhost:1338/roundtrip-test + + ./app/crane pull --format=oci $img $layout + ./app/crane push --image-refs=foo.images $layout $dst + diff <(./app/crane manifest $img) <(./app/crane manifest $(cat foo.images)) + + # Make sure we can roundtrip an index (distroless). + distroless=$(mktemp -d) + remote="gcr.io/distroless/static" + local="localhost:1338/distroless:static" + + ./app/crane pull --format=oci $remote $distroless + ./app/crane push $distroless $local + diff <(./app/crane manifest $remote) <(./app/crane manifest $local) + + # And that it works for a single platform (pulling from what we just pushed). + distroless=$(mktemp -d) + remote="$local" + local="localhost:1338/distroless/platform:static" + + ./app/crane pull --platform=linux/arm64 --format=oci $remote $distroless + ./app/crane push $distroless $local + diff <(./app/crane manifest --platform linux/arm64 $remote) <(./app/crane manifest $local) + + - name: crane pull image, and export it from stdin to filesystem tar to stdout + shell: bash + run: | + set -euxo pipefail + + ./app/crane pull ubuntu ubuntu.tar + ./app/crane export - - < ubuntu.tar > filesystem.tar + ls -la *.tar + diff --git a/.github/workflows/ecr-auth.yaml b/.github/workflows/ecr-auth.yaml new file mode 100644 index 0000000..47cfe29 --- /dev/null +++ b/.github/workflows/ecr-auth.yaml @@ -0,0 +1,93 @@ +name: ECR Authentication test + +on: + pull_request_target: + branches: [ 'main' ] + +permissions: + # This lets us clone the repo + contents: read + # This lets us mint identity tokens. + id-token: write + +jobs: + krane: + runs-on: ubuntu-latest + env: + AWS_ACCOUNT: 479305788615 + AWS_REGION: us-east-2 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - name: Install krane + working-directory: ./cmd/krane + run: go install . + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2.0.0 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/federated-ecr-readonly + aws-region: ${{ env.AWS_REGION }} + + - name: Test krane + ECR + run: | + # List the tags + krane ls ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/go-containerregistry-test + + - name: Test krane auth get + ECR + shell: bash + run: | + CRED1=$(krane auth get ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com) + CRED2=$(krane auth get ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com) + if [[ "$CRED1" == "" ]] ; then + exit 1 + fi + if [[ "$CRED1" == "$CRED2" ]] ; then + echo "credentials are cached by infrastructure" + fi + + crane-ecr-login: + runs-on: ubuntu-latest + env: + AWS_ACCOUNT: 479305788615 + AWS_REGION: us-east-2 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - name: Install crane + working-directory: ./cmd/crane + run: go install . + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2.0.0 + with: + role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT }}:role/federated-ecr-readonly + aws-region: ${{ env.AWS_REGION }} + + - run: | + wget https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com/0.5.0/linux-amd64/docker-credential-ecr-login + chmod +x ./docker-credential-ecr-login + mv docker-credential-ecr-login /usr/local/bin + + cat > $HOME/.docker/config.json <<EOF + { + "credHelpers": { + "${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com": "ecr-login" + } + } + EOF + + - name: Test crane + ECR + run: | + # List the tags + crane ls ${{ env.AWS_ACCOUNT }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/go-containerregistry-test diff --git a/.github/workflows/ghcr-auth.yaml b/.github/workflows/ghcr-auth.yaml new file mode 100644 index 0000000..a511827 --- /dev/null +++ b/.github/workflows/ghcr-auth.yaml @@ -0,0 +1,47 @@ +name: GHCR Authentication test + +on: + pull_request_target: + branches: ['main'] + push: + branches: ['main'] + +permissions: + contents: read + packages: read + +jobs: + krane: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - name: Install krane + working-directory: ./cmd/krane + run: go install . + + - name: Test krane + GHCR + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + # List the tags + krane ls ghcr.io/${{ github.repository }}/testimage + + - name: Test krane auth get + GHCR + env: + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + CRED1=$(krane auth get ghcr.io) + CRED2=$(krane auth get ghcr.io) + if [[ "$CRED1" == "" ]] ; then + exit 1 + fi + if [[ "$CRED1" == "$CRED2" ]] ; then + echo "credentials are cached by infrastructure" + fi + diff --git a/.github/workflows/presubmit.yaml b/.github/workflows/presubmit.yaml new file mode 100644 index 0000000..7771a7f --- /dev/null +++ b/.github/workflows/presubmit.yaml @@ -0,0 +1,34 @@ +# Copyright 2021 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Presubmit + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + presubmit: + name: Presubmit + runs-on: 'ubuntu-latest' + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + - run: ./hack/presubmit.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..abdaad3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,78 @@ +name: goreleaser + +on: + push: + tags: ['*'] + +jobs: + goreleaser: + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + steps: + - uses: actions/checkout@v3 + - name: Unshallow + run: git fetch --prune --unshallow + - uses: actions/setup-go@v3 + with: + go-version: 1.18 + check-latest: true + - uses: goreleaser/goreleaser-action@v4.2.0 + id: run-goreleaser + with: + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Generate subject + id: hash + env: + ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" + run: | + set -euo pipefail + + checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') + echo "::set-output name=hashes::$(cat $checksum_file | base64 -w0)" + + provenance: + needs: [goreleaser] + permissions: + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + contents: write # To add assets to a release. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.5.0 + with: + base64-subjects: "${{ needs.goreleaser.outputs.hashes }}" + upload-assets: true # upload to a new release + + verification: + needs: [goreleaser, provenance] + runs-on: ubuntu-latest + permissions: read-all + steps: + # Note: this will be replaced with the GHA in the future. + # See https://github.com/slsa-framework/slsa-verifier/issues/95 + - name: Install SLSA verifier + uses: slsa-framework/slsa-verifier/actions/installer@v2.0.1 + - name: Download assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz" + gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "multiple.intoto.jsonl" + - name: Verify assets + env: + CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }} + PROVENANCE: "${{ needs.provenance.outputs.attestation-name }}" + run: | + set -euo pipefail + checksums=$(echo "$CHECKSUMS" | base64 -d) + while read -r line; do + fn=$(echo $line | cut -d ' ' -f2) + echo "Verifying $fn" + ./slsa-verifier-linux-amd64 -artifact-path "$fn" \ + -provenance "$PROVENANCE" \ + -source "github.com/$GITHUB_REPOSITORY" \ + -tag "$GITHUB_REF_NAME" + done <<<"$checksums" diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..38d76ab --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,30 @@ +name: 'Close stale' + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + stale: + runs-on: 'ubuntu-latest' + steps: + - uses: 'actions/stale@v7' + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + stale-issue-message: |- + This issue is stale because it has been open for 90 days with no + activity. It will automatically close after 30 more days of + inactivity. Keep fresh with the 'lifecycle/frozen' label. + stale-issue-label: 'lifecycle/stale' + exempt-issue-labels: 'lifecycle/frozen' + + stale-pr-message: |- + This Pull Request is stale because it has been open for 90 days with + no activity. It will automatically close after 30 more days of + inactivity. Keep fresh with the 'lifecycle/frozen' label. + stale-pr-label: 'lifecycle/stale' + exempt-pr-labels: 'lifecycle/frozen' + + days-before-stale: 90 + days-before-close: 30 diff --git a/.github/workflows/style.yaml b/.github/workflows/style.yaml new file mode 100644 index 0000000..baf54c9 --- /dev/null +++ b/.github/workflows/style.yaml @@ -0,0 +1,55 @@ +name: Code Style + +on: + pull_request: + branches: ['main'] + +jobs: + + goimports: + name: check goimports + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + - uses: actions/checkout@v3 + - uses: chainguard-dev/actions/goimports@5e21cb47971231c078a677dfe89a348371cb880c # main + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: 1.19 + check-latest: true + + - uses: golangci/golangci-lint-action@v3.4.0 + with: + version: v1.51.2 + + - uses: reviewdog/action-misspell@v1 + if: ${{ always() }} + with: + github_token: ${{ secrets.github_token }} + fail_on_error: true + locale: "US" + exclude: ./vendor/* + + - uses: chainguard-dev/actions/trailing-space@5e21cb47971231c078a677dfe89a348371cb880c # main + if: ${{ always() }} + + - uses: chainguard-dev/actions/eof-newline@5e21cb47971231c078a677dfe89a348371cb880c # main + if: ${{ always() }} + + - uses: get-woke/woke-action-reviewdog@v0 + if: ${{ always() }} + with: + github-token: ${{ secrets.github_token }} + reporter: github-pr-check + level: error + fail-on-error: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..8f31fd2 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,28 @@ +name: Test + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + + test: + strategy: + matrix: + go-version: [1.19, '1.20'] + + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + + - run: go test -coverprofile=coverage.txt -covermode=atomic -race ./... + + - uses: codecov/codecov-action@v3.1.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6d410b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +**/*~ +.project +bazel* +.idea +*.iml + +cmd/crane/crane +cmd/gcrane/gcrane +cmd/krane/krane + +/.pc/ +/_build/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..1dee826 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,40 @@ +run: + timeout: 5m + + skip-dirs: + - internal + - pkg/registry + +issues: + exclude-rules: + - path: test # Excludes /test, *_test.go etc. + linters: + - gosec + +linters: + enable: + - asciicheck + - depguard + - errorlint + - gofmt + - gosec + - goimports + - importas + - prealloc + - revive + - misspell + - stylecheck + - tparallel + - unconvert + - unparam + - unused + - whitespace + + disable: + - errcheck + +linters-settings: + depguard: + include-go-root: true + packages-with-error-message: + - crypto/sha256: "use crypto.SHA256 instead" diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..f078121 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,119 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +# before: +# hooks: +# # You may remove this if you don't use go modules. +# - go mod download +# # you may remove this if you don't need go generate +# - go generate ./... +builds: +- id: crane + env: + - CGO_ENABLED=0 + main: ./cmd/crane/main.go + binary: crane + flags: + - -trimpath + ldflags: + - -s + - -w + - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}} + - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}} + goarch: + - amd64 + - arm + - arm64 + - 386 + - s390x + goos: + - linux + - darwin + - windows + ignore: + - goos: windows + goarch: 386 + +- id: gcrane + env: + - CGO_ENABLED=0 + main: ./cmd/gcrane/main.go + binary: gcrane + flags: + - -trimpath + ldflags: + - -s + - -w + - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}} + - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}} + goarch: + - amd64 + - arm + - arm64 + - 386 + - s390x + goos: + - linux + - darwin + - windows + ignore: + - goos: windows + goarch: 386 + +- id: krane + env: + - CGO_ENABLED=0 + main: ./main.go + dir: ./cmd/krane + binary: krane + flags: + - -trimpath + ldflags: + - -s + - -w + - -X github.com/google/go-containerregistry/cmd/crane/cmd.Version={{.Version}} + - -X github.com/google/go-containerregistry/pkg/v1/remote/transport.Version={{.Version}} + goarch: + - amd64 + - arm + - arm64 + - 386 + - s390x + goos: + - linux + - darwin + - windows + ignore: + - goos: windows + goarch: 386 +source: + enabled: true +archives: +- replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' +release: + footer: | + ### Container Images + + https://gcr.io/go-containerregistry/crane:{{.Tag}} + https://gcr.io/go-containerregistry/gcrane:{{.Tag}} + + For example: + ``` + docker pull gcr.io/go-containerregistry/crane:{{.Tag}} + docker pull gcr.io/go-containerregistry/gcrane:{{.Tag}} + ``` diff --git a/.ko/debug/.ko.yaml b/.ko/debug/.ko.yaml new file mode 100644 index 0000000..ded0f59 --- /dev/null +++ b/.ko/debug/.ko.yaml @@ -0,0 +1 @@ +defaultBaseImage: gcr.io/distroless/base:debug diff --git a/.wokeignore b/.wokeignore new file mode 100644 index 0000000..05b7efe --- /dev/null +++ b/.wokeignore @@ -0,0 +1 @@ +vendor/** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..29e762c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# How to Contribute to go-containerregistry + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to <https://cla.developers.google.com/> to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Testing + +Ensure the following passes: +``` +./hack/presubmit.sh +``` +and commit any resultant changes to `go.mod` and `go.sum`. To update any docs +after client changes, run: + +``` +./hack/update-codegen.sh +``` @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.
\ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c77c03e --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# go-containerregistry + +[![GitHub Actions Build Status](https://github.com/google/go-containerregistry/workflows/Build/badge.svg)](https://github.com/google/go-containerregistry/actions?query=workflow%3ABuild) +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry?status.svg)](https://godoc.org/github.com/google/go-containerregistry) +[![Code Coverage](https://codecov.io/gh/google/go-containerregistry/branch/main/graph/badge.svg)](https://codecov.io/gh/google/go-containerregistry) + +## Introduction + +This is a golang library for working with container registries. +It's largely based on the [Python library of the same name](https://github.com/google/containerregistry). + +The following diagram shows the main types that this library handles. +![OCI image representation](images/ociimage.jpeg) + +## Philosophy + +The overarching design philosophy of this library is to define interfaces that present an immutable +view of resources (e.g. [`Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Image), +[`Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Layer), +[`ImageIndex`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#ImageIndex)), +which can be backed by a variety of medium (e.g. [registry](./pkg/v1/remote/README.md), +[tarball](./pkg/v1/tarball/README.md), [daemon](./pkg/v1/daemon/README.md), ...). + +To complement these immutable views, we support functional mutations that produce new immutable views +of the resulting resource (e.g. [mutate](./pkg/v1/mutate/README.md)). The end goal is to provide a +set of versatile primitives that can compose to do extraordinarily powerful things efficiently and easily. + +Both the resource views and mutations may be lazy, eager, memoizing, etc, and most are optimized +for common paths based on the tooling we have seen in the wild (e.g. writing new images from disk +to the registry as a compressed tarball). + + +### Experiments + +Over time, we will add new functionality under experimental environment variables listed here. + +| Env Var | Value(s) | What is does | +|---------|----------|--------------| +| `GGCR_EXPERIMENT_ESTARGZ` | `"1"` | When enabled this experiment will direct `tarball.LayerFromOpener` to emit [estargz](https://github.com/opencontainers/image-spec/issues/815) compatible layers, which enable them to be lazily loaded by an appropriately configured containerd. | + + +### `v1.Image` + +#### Sources + +* [`remote.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Image) +* [`tarball.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Image) +* [`daemon.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon#Image) +* [`layout.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Path.Image) +* [`random.Image`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Image) + +#### Sinks + +* [`remote.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Write) +* [`tarball.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Write) +* [`daemon.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon#Write) +* [`legacy/tarball.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball#Write) +* [`layout.AppendImage`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Path.AppendImage) + +### `v1.ImageIndex` + +#### Sources + +* [`remote.Index`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Index) +* [`random.Index`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Index) +* [`layout.ImageIndexFromPath`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#ImageIndexFromPath) + +#### Sinks + +* [`remote.WriteIndex`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#WriteIndex) +* [`layout.Write`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout#Write) + +### `v1.Layer` + +#### Sources + +* [`remote.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Layer) +* [`tarball.LayerFromFile`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#LayerFromFile) +* [`random.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/random#Layer) +* [`stream.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream#Layer) + +#### Sinks + +* [`remote.WriteLayer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#WriteLayer) + +## Overview + +### `mutate` + +The simplest use for these libraries is to read from one source and write to another. + +For example, + + * `crane pull` is `remote.Image -> tarball.Write`, + * `crane push` is `tarball.Image -> remote.Write`, + * `crane cp` is `remote.Image -> remote.Write`. + +However, often you actually want to _change something_ about an image. +This is the purpose of the [`mutate`](pkg/v1/mutate) package, which exposes +some commonly useful things to change about an image. + +### `partial` + +If you're trying to use this library with a different source or sink than it already supports, +it can be somewhat cumbersome. The `Image` and `Layer` interfaces are pretty wide, with a lot +of redundant information. This is somewhat by design, because we want to expose this information +as efficiently as possible where we can, but again it is a pain to implement yourself. + +The purpose of the [`partial`](pkg/v1/partial) package is to make implementing a `v1.Image` +much easier, by filling in all the derived accessors for you if you implement a minimal +subset of `v1.Image`. + +### `transport` + +You might think our abstractions are bad and you just want to authenticate +and send requests to a registry. + +This is the purpose of the [`transport`](pkg/v1/remote/transport) and [`authn`](pkg/authn) packages. + +## Tools + +This repo hosts some tools built on top of the library. + +### `crane` + +[`crane`](cmd/crane/README.md) is a tool for interacting with remote images +and registries. + +### `gcrane` + +[`gcrane`](cmd/gcrane/README.md) is a GCR-specific variant of `crane` that has +richer output for the `ls` subcommand and some basic garbage collection support. + +### `krane` + +[`krane`](cmd/krane/README.md) is a drop-in replacement for `crane` that supports +common Kubernetes-based workload identity mechanisms using [`k8schain`](#k8schain) +as a fallback to traditional authentication mechanisms. + +### `k8schain` + +[`k8schain`](pkg/authn/k8schain/README.md) implements the authentication +semantics used by kubelets in a way that is easily consumable by this library. + +`k8schain` is not a standalone tool, but it is linked here for visibility. + +### Emeritus: [`ko`](https://github.com/google/ko) + +This tool was originally developed in this repo but has since been moved to its +own repo. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ce1f393 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,4 @@ +To report a security issue, please use http://g.co/vulnz. We use +http://g.co/vulnz for our intake, and do coordination and disclosure here on +GitHub (including using GitHub Security Advisory). The Google Security Team will +respond within 5 working days of your report on g.co/vulnz. diff --git a/cloudbuild.yaml b/cloudbuild.yaml new file mode 100644 index 0000000..b9f2ad7 --- /dev/null +++ b/cloudbuild.yaml @@ -0,0 +1,61 @@ +timeout: 3600s # 60 minutes + +steps: +- name: golang + entrypoint: sh + args: + - -c + - | + set -eux + + export GOROOT=/usr/local/go + export KO_DOCKER_REPO="gcr.io/$PROJECT_ID" + export GOFLAGS="-ldflags=-X=github.com/google/go-containerregistry/cmd/crane/cmd.Version=$COMMIT_SHA" + + # Put contents of /workspace on GOPATH. + shadow=$$GOPATH/src/github.com/google/go-containerregistry + link_dir=$$(dirname "$$shadow") + mkdir -p $$link_dir + ln -s $$PWD $$shadow || stat $$shadow + + # Install ko from release. + curl -L -o ko.tar.gz https://github.com/google/ko/releases/download/v0.8.2/ko_0.8.2_Linux_i386.tar.gz + tar xvfz ko.tar.gz + chmod +x ko + alias ko=$${PWD}/ko + + # Use the ko binary to build the crane-ish builder images. + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/crane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/gcrane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + # ./cmd/krane is a separate module, so switch directories. + cd ./cmd/krane + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/krane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + cd ../../ + + # Use the ko binary to build the crane-ish builder *debug* images. + export KO_CONFIG_PATH=$(pwd)/.ko/debug/ + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/crane -t "debug" + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/gcrane -t "debug" + # ./cmd/krane is a separate module, so switch directories. + cd ./cmd/krane + ko publish --platform=all -B github.com/google/go-containerregistry/cmd/krane -t "debug" + cd ../../ + + # Tag-specific debug images are pushed to gcr.io/go-containerregistry/TOOL/debug:... + KO_DOCKER_REPO=gcr.io/$PROJECT_ID/crane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/crane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + KO_DOCKER_REPO=gcr.io/$PROJECT_ID/gcrane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/gcrane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + # ./cmd/krane is a separate module, so switch directories. + cd ./cmd/krane + KO_DOCKER_REPO=gcr.io/$PROJECT_ID/krane/debug ko publish --platform=all --bare github.com/google/go-containerregistry/cmd/krane -t latest -t "$COMMIT_SHA" -t "$TAG_NAME" + cd ../../ + +# Use the crane builder to get the digest for crane-ish. +- name: gcr.io/$PROJECT_ID/crane + args: ['digest', 'gcr.io/$PROJECT_ID/crane'] + +- name: gcr.io/$PROJECT_ID/crane + args: ['digest', 'gcr.io/$PROJECT_ID/gcrane'] + +- name: gcr.io/$PROJECT_ID/crane + args: ['digest', 'gcr.io/$PROJECT_ID/krane'] + diff --git a/cmd/crane/README.md b/cmd/crane/README.md new file mode 100644 index 0000000..a05dbeb --- /dev/null +++ b/cmd/crane/README.md @@ -0,0 +1,122 @@ +# `crane` + +[`crane`](doc/crane.md) is a tool for interacting with remote images +and registries. + +<img src="../../images/crane.png" width="40%"> + +A collection of useful things you can do with `crane` is [here](recipes.md). + +## Installation + +### Install from Releases + +1. Get the [latest release](https://github.com/google/go-containerregistry/releases/latest) version. + + ```sh + $ VERSION=$(curl -s "https://api.github.com/repos/google/go-containerregistry/releases/latest" | jq -r '.tag_name') + ``` + + or set a specific version: + + ```sh + $ VERSION=vX.Y.Z # Version number with a leading v + ``` + +1. Download the release. + + ```sh + $ OS=Linux # or Darwin, Windows + $ ARCH=x86_64 # or arm64, x86_64, armv6, i386, s390x + $ curl -sL "https://github.com/google/go-containerregistry/releases/download/${VERSION}/go-containerregistry_${OS}_${ARCH}.tar.gz" > go-containerregistry.tar.gz + ``` + +1. Verify the signature. We generate [SLSA 3 provenance](https://slsa.dev) using + the OpenSSF's [slsa-framework/slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator). + To verify our release, install the verification tool from [slsa-framework/slsa-verifier#installation](https://github.com/slsa-framework/slsa-verifier#installation) + and verify as follows: + + ```sh + $ curl -sL https://github.com/google/go-containerregistry/releases/download/${VERSION}/multiple.intoto.jsonl > provenance.intoto.jsonl + $ # NOTE: You may be using a different architecture. + $ slsa-verifier-linux-amd64 verify-artifact go-containerregistry.tar.gz --provenance-path provenance.intoto.jsonl --source-uri github.com/google/go-containerregistry --source-tag "${VERSION}" + PASSED: Verified SLSA provenance + ``` + +1. Unpack it in the PATH. + + ```sh + $ tar -zxvf go-containerregistry.tar.gz -C /usr/local/bin/ crane + ``` + +### Install manually + +Install manually: + +```sh +go install github.com/google/go-containerregistry/cmd/crane@latest +``` + +### Install via brew + +If you're macOS user and using [Homebrew](https://brew.sh/), you can install via brew command: + +```sh +$ brew install crane +``` + +### Install on Arch Linux + +If you're an Arch Linux user you can install via pacman command: + +```sh +$ pacman -S crane +``` + +### Setup on GitHub Actions + +You can use the [`setup-crane`](https://github.com/imjasonh/setup-crane) action +to install `crane` and setup auth to [GitHub Container +Registry](https://github.com/features/packages) in a GitHub Action workflow: + +``` +steps: +- uses: imjasonh/setup-crane@v0.1 +``` + +## Images + +You can also use crane as docker image + +```sh +$ docker run --rm gcr.io/go-containerregistry/crane ls ubuntu +10.04 +12.04.5 +12.04 +12.10 +``` + +And it's also available with a shell, at the `:debug` tag: + +```sh +docker run --rm -it --entrypoint "/busybox/sh" gcr.io/go-containerregistry/crane:debug +``` + +Tagged debug images are available at `gcr.io/go-containerregistry/crane/debug:[tag]`. + +### Using with GitLab + +```yaml +# Tags an existing Docker image which was tagged with the short commit hash with the tag 'latest' +docker-tag-latest: + stage: latest + only: + refs: + - main + image: + name: gcr.io/go-containerregistry/crane:debug + entrypoint: [""] + script: + - crane auth login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - crane tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA latest +``` diff --git a/cmd/crane/cmd/append.go b/cmd/crane/cmd/append.go new file mode 100644 index 0000000..9eb2acc --- /dev/null +++ b/cmd/crane/cmd/append.go @@ -0,0 +1,122 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/types" + specsv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" +) + +// NewCmdAppend creates a new cobra.Command for the append subcommand. +func NewCmdAppend(options *[]crane.Option) *cobra.Command { + var baseRef, newTag, outFile string + var newLayers []string + var annotate, ociEmptyBase bool + + appendCmd := &cobra.Command{ + Use: "append", + Short: "Append contents of a tarball to a remote image", + Long: `This sub-command pushes an image based on an (optional) +base image, with appended layers containing the contents of the +provided tarballs. + +If the base image is a Windows base image (i.e., its config.OS is "windows"), +the contents of the tarballs will be modified to be suitable for a Windows +container image.`, + Args: cobra.NoArgs, + RunE: func(_ *cobra.Command, args []string) error { + var base v1.Image + var err error + + if baseRef == "" { + logs.Warn.Printf("base unspecified, using empty image") + base = empty.Image + if ociEmptyBase { + base = mutate.MediaType(base, types.OCIManifestSchema1) + base = mutate.ConfigMediaType(base, types.OCIConfigJSON) + } + } else { + base, err = crane.Pull(baseRef, *options...) + if err != nil { + return fmt.Errorf("pulling %s: %w", baseRef, err) + } + } + + img, err := crane.Append(base, newLayers...) + if err != nil { + return fmt.Errorf("appending %v: %w", newLayers, err) + } + + if baseRef != "" && annotate { + ref, err := name.ParseReference(baseRef) + if err != nil { + return fmt.Errorf("parsing ref %q: %w", baseRef, err) + } + + baseDigest, err := base.Digest() + if err != nil { + return err + } + anns := map[string]string{ + specsv1.AnnotationBaseImageDigest: baseDigest.String(), + } + if _, ok := ref.(name.Tag); ok { + anns[specsv1.AnnotationBaseImageName] = ref.Name() + } + img = mutate.Annotations(img, anns).(v1.Image) + } + + if outFile != "" { + if err := crane.Save(img, newTag, outFile); err != nil { + return fmt.Errorf("writing output %q: %w", outFile, err) + } + } else { + if err := crane.Push(img, newTag, *options...); err != nil { + return fmt.Errorf("pushing image %s: %w", newTag, err) + } + ref, err := name.ParseReference(newTag) + if err != nil { + return fmt.Errorf("parsing reference %s: %w", newTag, err) + } + d, err := img.Digest() + if err != nil { + return fmt.Errorf("digest: %w", err) + } + fmt.Println(ref.Context().Digest(d.String())) + } + return nil + }, + } + appendCmd.Flags().StringVarP(&baseRef, "base", "b", "", "Name of base image to append to") + appendCmd.Flags().StringVarP(&newTag, "new_tag", "t", "", "Tag to apply to resulting image") + appendCmd.Flags().StringSliceVarP(&newLayers, "new_layer", "f", []string{}, "Path to tarball to append to image") + appendCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image") + appendCmd.Flags().BoolVar(&annotate, "set-base-image-annotations", false, "If true, annotate the resulting image as being based on the base image") + appendCmd.Flags().BoolVar(&ociEmptyBase, "oci-empty-base", false, "If true, empty base image will have OCI media types instead of Docker") + + appendCmd.MarkFlagsMutuallyExclusive("oci-empty-base", "base") + appendCmd.MarkFlagRequired("new_tag") + appendCmd.MarkFlagRequired("new_layer") + return appendCmd +} diff --git a/cmd/crane/cmd/auth.go b/cmd/crane/cmd/auth.go new file mode 100644 index 0000000..1964ade --- /dev/null +++ b/cmd/crane/cmd/auth.go @@ -0,0 +1,205 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "os" + "strings" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/types" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/cobra" +) + +// NewCmdAuth creates a new cobra.Command for the auth subcommand. +func NewCmdAuth(options []crane.Option, argv ...string) *cobra.Command { + cmd := &cobra.Command{ + Use: "auth", + Short: "Log in or access credentials", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Usage() }, + } + cmd.AddCommand(NewCmdAuthGet(options, argv...), NewCmdAuthLogin(argv...)) + return cmd +} + +type credentials struct { + Username string + Secret string +} + +// https://github.com/docker/cli/blob/2291f610ae73533e6e0749d4ef1e360149b1e46b/cli/config/credentials/native_store.go#L100-L109 +func toCreds(config *authn.AuthConfig) credentials { + creds := credentials{ + Username: config.Username, + Secret: config.Password, + } + + if config.IdentityToken != "" { + creds.Username = "<token>" + creds.Secret = config.IdentityToken + } + return creds +} + +// NewCmdAuthGet creates a new `crane auth get` command. +func NewCmdAuthGet(options []crane.Option, argv ...string) *cobra.Command { + if len(argv) == 0 { + argv = []string{os.Args[0]} + } + + baseCmd := strings.Join(argv, " ") + eg := fmt.Sprintf(` # Read configured credentials for reg.example.com + $ echo "reg.example.com" | %s get + {"username":"AzureDiamond","password":"hunter2"} + # or + $ %s get reg.example.com + {"username":"AzureDiamond","password":"hunter2"}`, baseCmd, baseCmd) + + return &cobra.Command{ + Use: "get [REGISTRY_ADDR]", + Short: "Implements a credential helper", + Example: eg, + Args: cobra.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + registryAddr := "" + if len(args) == 1 { + registryAddr = args[0] + } else { + b, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + registryAddr = strings.TrimSpace(string(b)) + } + + reg, err := name.NewRegistry(registryAddr) + if err != nil { + return err + } + authorizer, err := crane.GetOptions(options...).Keychain.Resolve(reg) + if err != nil { + return err + } + + // If we don't find any credentials, there's a magic error to return: + // + // https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/error.go#L4-L6 + // https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/credentials.go#L61-L63 + if authorizer == authn.Anonymous { + fmt.Fprint(os.Stdout, "credentials not found in native keychain\n") + os.Exit(1) + } + + auth, err := authorizer.Authorization() + if err != nil { + return err + } + + // Convert back to a form that credential helpers can parse so that this + // can act as a meta credential helper. + creds := toCreds(auth) + return json.NewEncoder(os.Stdout).Encode(creds) + }, + } +} + +// NewCmdAuthLogin creates a new `crane auth login` command. +func NewCmdAuthLogin(argv ...string) *cobra.Command { + var opts loginOptions + + if len(argv) == 0 { + argv = []string{os.Args[0]} + } + + eg := fmt.Sprintf(` # Log in to reg.example.com + %s login reg.example.com -u AzureDiamond -p hunter2`, strings.Join(argv, " ")) + + cmd := &cobra.Command{ + Use: "login [OPTIONS] [SERVER]", + Short: "Log in to a registry", + Example: eg, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + reg, err := name.NewRegistry(args[0]) + if err != nil { + return err + } + + opts.serverAddress = reg.Name() + + return login(opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.user, "username", "u", "", "Username") + flags.StringVarP(&opts.password, "password", "p", "", "Password") + flags.BoolVarP(&opts.passwordStdin, "password-stdin", "", false, "Take the password from stdin") + + return cmd +} + +type loginOptions struct { + serverAddress string + user string + password string + passwordStdin bool +} + +func login(opts loginOptions) error { + if opts.passwordStdin { + contents, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + + opts.password = strings.TrimSuffix(string(contents), "\n") + opts.password = strings.TrimSuffix(opts.password, "\r") + } + if opts.user == "" && opts.password == "" { + return errors.New("username and password required") + } + cf, err := config.Load(os.Getenv("DOCKER_CONFIG")) + if err != nil { + return err + } + creds := cf.GetCredentialsStore(opts.serverAddress) + if opts.serverAddress == name.DefaultRegistry { + opts.serverAddress = authn.DefaultAuthKey + } + if err := creds.Store(types.AuthConfig{ + ServerAddress: opts.serverAddress, + Username: opts.user, + Password: opts.password, + }); err != nil { + return err + } + + if err := cf.Save(); err != nil { + return err + } + log.Printf("logged in via %s", cf.Filename) + return nil +} diff --git a/cmd/crane/cmd/blob.go b/cmd/crane/cmd/blob.go new file mode 100644 index 0000000..fef405f --- /dev/null +++ b/cmd/crane/cmd/blob.go @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdBlob creates a new cobra.Command for the blob subcommand. +func NewCmdBlob(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "blob BLOB", + Short: "Read a blob from the registry", + Example: "crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + src := args[0] + layer, err := crane.PullLayer(src, *options...) + if err != nil { + return fmt.Errorf("pulling layer %s: %w", src, err) + } + blob, err := layer.Compressed() + if err != nil { + return fmt.Errorf("fetching blob %s: %w", src, err) + } + if _, err := io.Copy(cmd.OutOrStdout(), blob); err != nil { + return fmt.Errorf("copying blob %s: %w", src, err) + } + return nil + }, + } +} diff --git a/cmd/crane/cmd/catalog.go b/cmd/crane/cmd/catalog.go new file mode 100644 index 0000000..0bfa0a9 --- /dev/null +++ b/cmd/crane/cmd/catalog.go @@ -0,0 +1,54 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdCatalog creates a new cobra.Command for the repos subcommand. +func NewCmdCatalog(options *[]crane.Option, argv ...string) *cobra.Command { + if len(argv) == 0 { + argv = []string{os.Args[0]} + } + + baseCmd := strings.Join(argv, " ") + eg := fmt.Sprintf(` # list the repos for reg.example.com + $ %s catalog reg.example.com`, baseCmd) + + return &cobra.Command{ + Use: "catalog [REGISTRY]", + Short: "List the repos in a registry", + Example: eg, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + reg := args[0] + repos, err := crane.Catalog(reg, *options...) + if err != nil { + return fmt.Errorf("reading repos for %s: %w", reg, err) + } + + for _, repo := range repos { + fmt.Println(repo) + } + return nil + }, + } +} diff --git a/cmd/crane/cmd/config.go b/cmd/crane/cmd/config.go new file mode 100644 index 0000000..ed2a3fb --- /dev/null +++ b/cmd/crane/cmd/config.go @@ -0,0 +1,39 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdConfig creates a new cobra.Command for the config subcommand. +func NewCmdConfig(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "config IMAGE", + Short: "Get the config of an image", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + cfg, err := crane.Config(args[0], *options...) + if err != nil { + return fmt.Errorf("fetching config: %w", err) + } + fmt.Print(string(cfg)) + return nil + }, + } +} diff --git a/cmd/crane/cmd/copy.go b/cmd/crane/cmd/copy.go new file mode 100644 index 0000000..81f2e70 --- /dev/null +++ b/cmd/crane/cmd/copy.go @@ -0,0 +1,34 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdCopy creates a new cobra.Command for the copy subcommand. +func NewCmdCopy(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "copy SRC DST", + Aliases: []string{"cp"}, + Short: "Efficiently copy a remote image from src to dst while retaining the digest value", + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + src, dst := args[0], args[1] + return crane.Copy(src, dst, *options...) + }, + } +} diff --git a/cmd/crane/cmd/delete.go b/cmd/crane/cmd/delete.go new file mode 100644 index 0000000..18966ea --- /dev/null +++ b/cmd/crane/cmd/delete.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdDelete creates a new cobra.Command for the delete subcommand. +func NewCmdDelete(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "delete IMAGE", + Short: "Delete an image reference from its registry", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + ref := args[0] + return crane.Delete(ref, *options...) + }, + } +} diff --git a/cmd/crane/cmd/digest.go b/cmd/crane/cmd/digest.go new file mode 100644 index 0000000..2060bbc --- /dev/null +++ b/cmd/crane/cmd/digest.go @@ -0,0 +1,91 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/cobra" +) + +// NewCmdDigest creates a new cobra.Command for the digest subcommand. +func NewCmdDigest(options *[]crane.Option) *cobra.Command { + var tarball string + var fullRef bool + cmd := &cobra.Command{ + Use: "digest IMAGE", + Short: "Get the digest of an image", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if tarball == "" && len(args) == 0 { + if err := cmd.Help(); err != nil { + return err + } + return errors.New("image reference required without --tarball") + } + if fullRef && tarball != "" { + return errors.New("cannot specify --full-ref with --tarball") + } + + digest, err := getDigest(tarball, args, options) + if err != nil { + return err + } + if fullRef { + ref, err := name.ParseReference(args[0]) + if err != nil { + return err + } + fmt.Println(ref.Context().Digest(digest)) + } else { + fmt.Println(digest) + } + return nil + }, + } + + cmd.Flags().StringVar(&tarball, "tarball", "", "(Optional) path to tarball containing the image") + cmd.Flags().BoolVar(&fullRef, "full-ref", false, "(Optional) if true, print the full image reference by digest") + + return cmd +} + +func getDigest(tarball string, args []string, options *[]crane.Option) (string, error) { + if tarball != "" { + return getTarballDigest(tarball, args, options) + } + + return crane.Digest(args[0], *options...) +} + +func getTarballDigest(tarball string, args []string, options *[]crane.Option) (string, error) { + tag := "" + if len(args) > 0 { + tag = args[0] + } + + img, err := crane.LoadTag(tarball, tag, *options...) + if err != nil { + return "", fmt.Errorf("loading image from %q: %w", tarball, err) + } + digest, err := img.Digest() + if err != nil { + return "", fmt.Errorf("computing digest: %w", err) + } + return digest.String(), nil +} diff --git a/cmd/crane/cmd/export.go b/cmd/crane/cmd/export.go new file mode 100644 index 0000000..70b58c1 --- /dev/null +++ b/cmd/crane/cmd/export.go @@ -0,0 +1,89 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/spf13/cobra" +) + +// NewCmdExport creates a new cobra.Command for the export subcommand. +func NewCmdExport(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "export IMAGE|- TARBALL|-", + Short: "Export filesystem of a container image as a tarball", + Example: ` # Write tarball to stdout + crane export ubuntu - + + # Write tarball to file + crane export ubuntu ubuntu.tar + + # Read image from stdin + crane export - ubuntu.tar`, + Args: cobra.RangeArgs(1, 2), + RunE: func(_ *cobra.Command, args []string) error { + src, dst := args[0], "-" + if len(args) > 1 { + dst = args[1] + } + + f, err := openFile(dst) + if err != nil { + return fmt.Errorf("failed to open %s: %w", dst, err) + } + defer f.Close() + + var img v1.Image + if src == "-" { + tmpfile, err := os.CreateTemp("", "crane") + if err != nil { + log.Fatal(err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := io.Copy(tmpfile, os.Stdin); err != nil { + log.Fatal(err) + } + tmpfile.Close() + + img, err = tarball.ImageFromPath(tmpfile.Name(), nil) + if err != nil { + return fmt.Errorf("reading tarball from stdin: %w", err) + } + } else { + img, err = crane.Pull(src, *options...) + if err != nil { + return fmt.Errorf("pulling %s: %w", src, err) + } + } + + return crane.Export(img, f) + }, + } +} + +func openFile(s string) (*os.File, error) { + if s == "-" { + return os.Stdout, nil + } + return os.Create(s) +} diff --git a/cmd/crane/cmd/flatten.go b/cmd/crane/cmd/flatten.go new file mode 100644 index 0000000..76c13f1 --- /dev/null +++ b/cmd/crane/cmd/flatten.go @@ -0,0 +1,254 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "log" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/spf13/cobra" +) + +// NewCmdFlatten creates a new cobra.Command for the flatten subcommand. +func NewCmdFlatten(options *[]crane.Option) *cobra.Command { + var dst string + + flattenCmd := &cobra.Command{ + Use: "flatten", + Short: "Flatten an image's layers into a single layer", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + // We need direct access to the underlying remote options because crane + // doesn't expose great facilities for working with an index (yet). + o := crane.GetOptions(*options...) + + // Pull image and get config. + src := args[0] + + // If the new ref isn't provided, write over the original image. + // If that ref was provided by digest (e.g., output from + // another crane command), then strip that and push the + // mutated image by digest instead. + if dst == "" { + dst = src + } + + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + log.Fatalf("parsing %s: %v", src, err) + } + newRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + log.Fatalf("parsing %s: %v", dst, err) + } + repo := newRef.Context() + + flat, err := flatten(ref, repo, cmd.Parent().Use, o) + if err != nil { + log.Fatalf("flattening %s: %v", ref, err) + } + + digest, err := flat.Digest() + if err != nil { + log.Fatalf("digesting new image: %v", err) + } + + if _, ok := ref.(name.Digest); ok { + newRef = repo.Digest(digest.String()) + } + + if err := push(flat, newRef, o); err != nil { + log.Fatalf("pushing %s: %v", newRef, err) + } + fmt.Println(repo.Digest(digest.String())) + }, + } + flattenCmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag to apply to flattened image. If not provided, push by digest to the original image repository.") + return flattenCmd +} + +func flatten(ref name.Reference, repo name.Repository, use string, o crane.Options) (partial.Describable, error) { + desc, err := remote.Get(ref, o.Remote...) + if err != nil { + return nil, fmt.Errorf("pulling %s: %w", ref, err) + } + + if desc.MediaType.IsIndex() { + idx, err := desc.ImageIndex() + if err != nil { + return nil, err + } + return flattenIndex(idx, repo, use, o) + } else if desc.MediaType.IsImage() { + img, err := desc.Image() + if err != nil { + return nil, err + } + return flattenImage(img, repo, use, o) + } + + return nil, fmt.Errorf("can't flatten %s", desc.MediaType) +} + +func push(flat partial.Describable, ref name.Reference, o crane.Options) error { + if idx, ok := flat.(v1.ImageIndex); ok { + return remote.WriteIndex(ref, idx, o.Remote...) + } else if img, ok := flat.(v1.Image); ok { + return remote.Write(ref, img, o.Remote...) + } + + return fmt.Errorf("can't push %T", flat) +} + +type remoteIndex interface { + Manifests() ([]partial.Describable, error) +} + +func flattenIndex(old v1.ImageIndex, repo name.Repository, use string, o crane.Options) (partial.Describable, error) { + ri, ok := old.(remoteIndex) + if !ok { + return nil, fmt.Errorf("unexpected index") + } + + m, err := old.IndexManifest() + if err != nil { + return nil, err + } + + manifests, err := ri.Manifests() + if err != nil { + return nil, err + } + + adds := []mutate.IndexAddendum{} + + for _, m := range manifests { + // Keep the old descriptor (annotations and whatnot). + desc, err := partial.Descriptor(m) + if err != nil { + return nil, err + } + + flattened, err := flattenChild(m, repo, use, o) + if err != nil { + return nil, err + } + desc.Size, err = flattened.Size() + if err != nil { + return nil, err + } + desc.Digest, err = flattened.Digest() + if err != nil { + return nil, err + } + adds = append(adds, mutate.IndexAddendum{ + Add: flattened, + Descriptor: *desc, + }) + } + + idx := mutate.AppendManifests(empty.Index, adds...) + + // Retain any annotations from the original index. + if len(m.Annotations) != 0 { + idx = mutate.Annotations(idx, m.Annotations).(v1.ImageIndex) + } + + // This is stupid, but some registries get mad if you try to push OCI media types that reference docker media types. + mt, err := old.MediaType() + if err != nil { + return nil, err + } + idx = mutate.IndexMediaType(idx, mt) + + return idx, nil +} + +func flattenChild(old partial.Describable, repo name.Repository, use string, o crane.Options) (partial.Describable, error) { + if idx, ok := old.(v1.ImageIndex); ok { + return flattenIndex(idx, repo, use, o) + } else if img, ok := old.(v1.Image); ok { + return flattenImage(img, repo, use, o) + } + + logs.Warn.Printf("can't flatten %T, skipping", old) + return old, nil +} + +func flattenImage(old v1.Image, repo name.Repository, use string, o crane.Options) (partial.Describable, error) { + digest, err := old.Digest() + if err != nil { + return nil, fmt.Errorf("getting old digest: %w", err) + } + m, err := old.Manifest() + if err != nil { + return nil, fmt.Errorf("reading manifest: %w", err) + } + + cf, err := old.ConfigFile() + if err != nil { + return nil, fmt.Errorf("getting config: %w", err) + } + cf = cf.DeepCopy() + + oldHistory, err := json.Marshal(cf.History) + if err != nil { + return nil, fmt.Errorf("marshal history") + } + + // Clear layer-specific config file information. + cf.RootFS.DiffIDs = []v1.Hash{} + cf.History = []v1.History{} + + img, err := mutate.ConfigFile(empty.Image, cf) + if err != nil { + return nil, fmt.Errorf("mutating config: %w", err) + } + + // TODO: Make compression configurable? + layer := stream.NewLayer(mutate.Extract(old), stream.WithCompressionLevel(gzip.BestCompression)) + if err := remote.WriteLayer(repo, layer, o.Remote...); err != nil { + return nil, fmt.Errorf("uploading layer: %w", err) + } + + img, err = mutate.Append(img, mutate.Addendum{ + Layer: layer, + History: v1.History{ + CreatedBy: fmt.Sprintf("%s flatten %s", use, digest), + Comment: string(oldHistory), + }, + }) + if err != nil { + return nil, fmt.Errorf("appending layers: %w", err) + } + + // Retain any annotations from the original image. + if len(m.Annotations) != 0 { + img = mutate.Annotations(img, m.Annotations).(v1.Image) + } + + return img, nil +} diff --git a/cmd/crane/cmd/index.go b/cmd/crane/cmd/index.go new file mode 100644 index 0000000..8d4b425 --- /dev/null +++ b/cmd/crane/cmd/index.go @@ -0,0 +1,291 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/spf13/cobra" +) + +// NewCmdIndex creates a new cobra.Command for the index subcommand. +func NewCmdIndex(options *[]crane.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "index", + Short: "Modify an image index.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, _ []string) { + cmd.Usage() + }, + } + cmd.AddCommand(NewCmdIndexFilter(options), NewCmdIndexAppend(options)) + return cmd +} + +// NewCmdIndexFilter creates a new cobra.Command for the index filter subcommand. +func NewCmdIndexFilter(options *[]crane.Option) *cobra.Command { + var newTag string + platforms := &platformsValue{} + + cmd := &cobra.Command{ + Use: "filter", + Short: "Modifies a remote index by filtering based on platform.", + Example: ` # Filter out weird platforms from ubuntu, copy result to example.com/ubuntu + crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t example.com/ubuntu + + # Filter out any non-linux platforms, push to example.com/hello-world + crane index filter hello-world --platform linux -t example.com/hello-world + + # Same as above, but in-place + crane index filter example.com/hello-world:some-tag --platform linux`, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + o := crane.GetOptions(*options...) + baseRef := args[0] + + ref, err := name.ParseReference(baseRef) + if err != nil { + return err + } + base, err := remote.Index(ref, o.Remote...) + if err != nil { + return fmt.Errorf("pulling %s: %w", baseRef, err) + } + + idx := filterIndex(base, platforms.platforms) + + digest, err := idx.Digest() + if err != nil { + return err + } + + if newTag != "" { + ref, err = name.ParseReference(newTag) + if err != nil { + return fmt.Errorf("parsing reference %s: %w", newTag, err) + } + } else { + if _, ok := ref.(name.Digest); ok { + ref = ref.Context().Digest(digest.String()) + } + } + + if err := remote.WriteIndex(ref, idx, o.Remote...); err != nil { + return fmt.Errorf("pushing image %s: %w", newTag, err) + } + fmt.Println(ref.Context().Digest(digest.String())) + return nil + }, + } + cmd.Flags().StringVarP(&newTag, "tag", "t", "", "Tag to apply to resulting image") + + // Consider reusing the persistent flag for this, it's separate so we can have multiple values. + cmd.Flags().Var(platforms, "platform", "Specifies the platform(s) to keep from base in the form os/arch[/variant][:osversion][,<platform>] (e.g. linux/amd64).") + + return cmd +} + +// NewCmdIndexAppend creates a new cobra.Command for the index append subcommand. +func NewCmdIndexAppend(options *[]crane.Option) *cobra.Command { + var baseRef, newTag string + var newManifests []string + var dockerEmptyBase, flatten bool + + cmd := &cobra.Command{ + Use: "append", + Short: "Append manifests to a remote index.", + Long: `This sub-command pushes an index based on an (optional) base index, with appended manifests. + +The platform for appended manifests is inferred from the config file or omitted if that is infeasible.`, + Example: ` # Append a windows hello-world image to ubuntu, push to example.com/hello-world:weird + crane index append ubuntu -m hello-world@sha256:87b9ca29151260634b95efb84d43b05335dc3ed36cc132e2b920dd1955342d20 -t example.com/hello-world:weird + + # Create an index from scratch for etcd. + crane index append -m registry.k8s.io/etcd-amd64:3.4.9 -m registry.k8s.io/etcd-arm64:3.4.9 -t example.com/etcd`, + Args: cobra.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if len(args) == 1 { + baseRef = args[0] + } + o := crane.GetOptions(*options...) + + var ( + base v1.ImageIndex + err error + ref name.Reference + ) + + if baseRef == "" { + if newTag == "" { + return errors.New("at least one of --base or --tag must be specified") + } + + logs.Warn.Printf("base unspecified, using empty index") + base = empty.Index + if dockerEmptyBase { + base = mutate.IndexMediaType(base, types.DockerManifestList) + } + } else { + ref, err = name.ParseReference(baseRef) + if err != nil { + return err + } + base, err = remote.Index(ref, o.Remote...) + if err != nil { + return fmt.Errorf("pulling %s: %w", baseRef, err) + } + } + + adds := make([]mutate.IndexAddendum, 0, len(newManifests)) + + for _, m := range newManifests { + ref, err := name.ParseReference(m) + if err != nil { + return err + } + desc, err := remote.Get(ref, o.Remote...) + if err != nil { + return err + } + if desc.MediaType.IsImage() { + img, err := desc.Image() + if err != nil { + return err + } + + cf, err := img.ConfigFile() + if err != nil { + return err + } + newDesc, err := partial.Descriptor(img) + if err != nil { + return err + } + newDesc.Platform = cf.Platform() + adds = append(adds, mutate.IndexAddendum{ + Add: img, + Descriptor: *newDesc, + }) + } else if desc.MediaType.IsIndex() { + idx, err := desc.ImageIndex() + if err != nil { + return err + } + if flatten { + im, err := idx.IndexManifest() + if err != nil { + return err + } + for _, child := range im.Manifests { + switch { + case child.MediaType.IsImage(): + img, err := idx.Image(child.Digest) + if err != nil { + return err + } + adds = append(adds, mutate.IndexAddendum{ + Add: img, + Descriptor: child, + }) + case child.MediaType.IsIndex(): + idx, err := idx.ImageIndex(child.Digest) + if err != nil { + return err + } + adds = append(adds, mutate.IndexAddendum{ + Add: idx, + Descriptor: child, + }) + default: + return fmt.Errorf("unexpected child %q with media type %q", child.Digest, child.MediaType) + } + } + } else { + adds = append(adds, mutate.IndexAddendum{ + Add: idx, + }) + } + } else { + return fmt.Errorf("saw unexpected MediaType %q for %q", desc.MediaType, m) + } + } + + idx := mutate.AppendManifests(base, adds...) + digest, err := idx.Digest() + if err != nil { + return err + } + + if newTag != "" { + ref, err = name.ParseReference(newTag) + if err != nil { + return fmt.Errorf("parsing reference %s: %w", newTag, err) + } + } else { + if _, ok := ref.(name.Digest); ok { + ref = ref.Context().Digest(digest.String()) + } + } + + if err := remote.WriteIndex(ref, idx, o.Remote...); err != nil { + return fmt.Errorf("pushing image %s: %w", newTag, err) + } + fmt.Println(ref.Context().Digest(digest.String())) + return nil + }, + } + cmd.Flags().StringVarP(&newTag, "tag", "t", "", "Tag to apply to resulting image") + cmd.Flags().StringSliceVarP(&newManifests, "manifest", "m", []string{}, "References to manifests to append to the base index") + cmd.Flags().BoolVar(&dockerEmptyBase, "docker-empty-base", false, "If true, empty base index will have Docker media types instead of OCI") + cmd.Flags().BoolVar(&flatten, "flatten", true, "If true, appending an index will append each of its children rather than the index itself") + + return cmd +} + +func filterIndex(idx v1.ImageIndex, platforms []v1.Platform) v1.ImageIndex { + matcher := not(satisfiesPlatforms(platforms)) + return mutate.RemoveManifests(idx, matcher) +} + +func satisfiesPlatforms(platforms []v1.Platform) match.Matcher { + return func(desc v1.Descriptor) bool { + if desc.Platform == nil { + return false + } + for _, p := range platforms { + if desc.Platform.Satisfies(p) { + return true + } + } + return false + } +} + +func not(in match.Matcher) match.Matcher { + return func(desc v1.Descriptor) bool { + return !in(desc) + } +} diff --git a/cmd/crane/cmd/list.go b/cmd/crane/cmd/list.go new file mode 100644 index 0000000..3902ccd --- /dev/null +++ b/cmd/crane/cmd/list.go @@ -0,0 +1,62 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/cobra" +) + +// NewCmdList creates a new cobra.Command for the ls subcommand. +func NewCmdList(options *[]crane.Option) *cobra.Command { + var fullRef, omitDigestTags bool + cmd := &cobra.Command{ + Use: "ls REPO", + Short: "List the tags in a repo", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + repo := args[0] + tags, err := crane.ListTags(repo, *options...) + if err != nil { + return fmt.Errorf("reading tags for %s: %w", repo, err) + } + + r, err := name.NewRepository(repo) + if err != nil { + return err + } + + for _, tag := range tags { + if omitDigestTags && strings.HasPrefix(tag, "sha256-") { + continue + } + + if fullRef { + fmt.Println(r.Tag(tag)) + } else { + fmt.Println(tag) + } + } + return nil + }, + } + cmd.Flags().BoolVar(&fullRef, "full-ref", false, "(Optional) if true, print the full image reference") + cmd.Flags().BoolVar(&omitDigestTags, "omit-digest-tags", false, "(Optional), if true, omit digest tags (e.g., ':sha256-...')") + return cmd +} diff --git a/cmd/crane/cmd/manifest.go b/cmd/crane/cmd/manifest.go new file mode 100644 index 0000000..d9ef7fd --- /dev/null +++ b/cmd/crane/cmd/manifest.go @@ -0,0 +1,40 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdManifest creates a new cobra.Command for the manifest subcommand. +func NewCmdManifest(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "manifest IMAGE", + Short: "Get the manifest of an image", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + src := args[0] + manifest, err := crane.Manifest(src, *options...) + if err != nil { + return fmt.Errorf("fetching manifest %s: %w", src, err) + } + fmt.Print(string(manifest)) + return nil + }, + } +} diff --git a/cmd/crane/cmd/mutate.go b/cmd/crane/cmd/mutate.go new file mode 100644 index 0000000..a99def0 --- /dev/null +++ b/cmd/crane/cmd/mutate.go @@ -0,0 +1,207 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/spf13/cobra" +) + +// NewCmdMutate creates a new cobra.Command for the mutate subcommand. +func NewCmdMutate(options *[]crane.Option) *cobra.Command { + var labels map[string]string + var annotations map[string]string + var entrypoint, cmd []string + var envVars map[string]string + var newLayers []string + var outFile string + var newRef string + var newRepo string + var user string + + mutateCmd := &cobra.Command{ + Use: "mutate", + Short: "Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there.", + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + // Pull image and get config. + ref := args[0] + + if len(annotations) != 0 { + desc, err := crane.Head(ref, *options...) + if err != nil { + return err + } + if desc.MediaType.IsIndex() { + return errors.New("mutating annotations on an index is not yet supported") + } + } + + if newRepo != "" && newRef != "" { + return errors.New("repository can't be set when a tag is specified") + } + + img, err := crane.Pull(ref, *options...) + if err != nil { + return fmt.Errorf("pulling %s: %w", ref, err) + } + if len(newLayers) != 0 { + img, err = crane.Append(img, newLayers...) + if err != nil { + return fmt.Errorf("appending %v: %w", newLayers, err) + } + } + cfg, err := img.ConfigFile() + if err != nil { + return err + } + cfg = cfg.DeepCopy() + + // Set labels. + if cfg.Config.Labels == nil { + cfg.Config.Labels = map[string]string{} + } + + if err := validateKeyVals(labels); err != nil { + return err + } + + for k, v := range labels { + cfg.Config.Labels[k] = v + } + + if err := validateKeyVals(annotations); err != nil { + return err + } + + // set envvars if specified + if err := setEnvVars(cfg, envVars); err != nil { + return err + } + + // Set entrypoint. + if len(entrypoint) > 0 { + cfg.Config.Entrypoint = entrypoint + cfg.Config.Cmd = nil // This matches Docker's behavior. + } + + // Set cmd. + if len(cmd) > 0 { + cfg.Config.Cmd = cmd + } + + // Set user. + if len(user) > 0 { + cfg.Config.User = user + } + + // Mutate and write image. + img, err = mutate.Config(img, cfg.Config) + if err != nil { + return fmt.Errorf("mutating config: %w", err) + } + + img = mutate.Annotations(img, annotations).(v1.Image) + + // If the new ref isn't provided, write over the original image. + // If that ref was provided by digest (e.g., output from + // another crane command), then strip that and push the + // mutated image by digest instead. + if newRepo != "" { + newRef = newRepo + } else if newRef == "" { + newRef = ref + } + digest, err := img.Digest() + if err != nil { + return fmt.Errorf("digesting new image: %w", err) + } + if outFile != "" { + if err := crane.Save(img, newRef, outFile); err != nil { + return fmt.Errorf("writing output %q: %w", outFile, err) + } + } else { + r, err := name.ParseReference(newRef) + if err != nil { + return fmt.Errorf("parsing %s: %w", newRef, err) + } + if _, ok := r.(name.Digest); ok || newRepo != "" { + newRef = r.Context().Digest(digest.String()).String() + } + if err := crane.Push(img, newRef, *options...); err != nil { + return fmt.Errorf("pushing %s: %w", newRef, err) + } + fmt.Println(r.Context().Digest(digest.String())) + } + return nil + }, + } + mutateCmd.Flags().StringToStringVarP(&annotations, "annotation", "a", nil, "New annotations to add") + mutateCmd.Flags().StringToStringVarP(&labels, "label", "l", nil, "New labels to add") + mutateCmd.Flags().StringToStringVarP(&envVars, "env", "e", nil, "New envvar to add") + mutateCmd.Flags().StringSliceVar(&entrypoint, "entrypoint", nil, "New entrypoint to set") + mutateCmd.Flags().StringSliceVar(&cmd, "cmd", nil, "New cmd to set") + mutateCmd.Flags().StringVar(&newRepo, "repo", "", "Repository to push the mutated image to. If provided, push by digest to this repository.") + mutateCmd.Flags().StringVarP(&newRef, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, push by digest to the original image repository.") + mutateCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image") + mutateCmd.Flags().StringSliceVar(&newLayers, "append", []string{}, "Path to tarball to append to image") + mutateCmd.Flags().StringVarP(&user, "user", "u", "", "New user to set") + return mutateCmd +} + +// validateKeyVals ensures no values are empty, returns error if they are +func validateKeyVals(kvPairs map[string]string) error { + for label, value := range kvPairs { + if value == "" { + return fmt.Errorf("parsing label %q, value is empty", label) + } + } + return nil +} + +// setEnvVars override envvars in a config +func setEnvVars(cfg *v1.ConfigFile, envVars map[string]string) error { + newEnv := make([]string, 0, len(cfg.Config.Env)) + for _, old := range cfg.Config.Env { + split := strings.SplitN(old, "=", 2) + if len(split) != 2 { + return fmt.Errorf("invalid key value pair in config: %s", old) + } + // keep order so override if specified again + oldKey := split[0] + if v, ok := envVars[oldKey]; ok { + newEnv = append(newEnv, fmt.Sprintf("%s=%s", oldKey, v)) + delete(envVars, oldKey) + } else { + newEnv = append(newEnv, old) + } + } + isWindows := cfg.OS == "windows" + for k, v := range envVars { + if isWindows { + k = strings.ToUpper(k) + } + newEnv = append(newEnv, fmt.Sprintf("%s=%s", k, v)) + } + cfg.Config.Env = newEnv + return nil +} diff --git a/cmd/crane/cmd/optimize.go b/cmd/crane/cmd/optimize.go new file mode 100644 index 0000000..89c4706 --- /dev/null +++ b/cmd/crane/cmd/optimize.go @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdOptimize creates a new cobra.Command for the optimize subcommand. +func NewCmdOptimize(options *[]crane.Option) *cobra.Command { + var files []string + + cmd := &cobra.Command{ + Use: "optimize SRC DST", + Hidden: true, + Aliases: []string{"opt"}, + Short: "Optimize a remote container image from src to dst", + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + src, dst := args[0], args[1] + return crane.Optimize(src, dst, files, *options...) + }, + } + + cmd.Flags().StringSliceVar(&files, "prioritize", nil, + "The list of files to prioritize in the optimized image.") + + return cmd +} diff --git a/cmd/crane/cmd/pull.go b/cmd/crane/cmd/pull.go new file mode 100644 index 0000000..41c6e95 --- /dev/null +++ b/cmd/crane/cmd/pull.go @@ -0,0 +1,138 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/cache" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/cobra" +) + +// NewCmdPull creates a new cobra.Command for the pull subcommand. +func NewCmdPull(options *[]crane.Option) *cobra.Command { + var ( + cachePath, format string + annotateRef bool + ) + + cmd := &cobra.Command{ + Use: "pull IMAGE TARBALL", + Short: "Pull remote images by reference and store their contents locally", + Args: cobra.MinimumNArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + imageMap := map[string]v1.Image{} + indexMap := map[string]v1.ImageIndex{} + srcList, path := args[:len(args)-1], args[len(args)-1] + o := crane.GetOptions(*options...) + for _, src := range srcList { + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + rmt, err := remote.Get(ref, o.Remote...) + if err != nil { + return err + } + + // If we're writing an index to a layout and --platform hasn't been set, + // pull the entire index, not just a child image. + if format == "oci" && rmt.MediaType.IsIndex() && o.Platform == nil { + idx, err := rmt.ImageIndex() + if err != nil { + return err + } + indexMap[src] = idx + continue + } + + img, err := rmt.Image() + if err != nil { + return err + } + if cachePath != "" { + img = cache.Image(img, cache.NewFilesystemCache(cachePath)) + } + imageMap[src] = img + } + + switch format { + case "tarball": + if err := crane.MultiSave(imageMap, path); err != nil { + return fmt.Errorf("saving tarball %s: %w", path, err) + } + case "legacy": + if err := crane.MultiSaveLegacy(imageMap, path); err != nil { + return fmt.Errorf("saving legacy tarball %s: %w", path, err) + } + case "oci": + // Don't use crane.MultiSaveOCI so we can control annotations. + p, err := layout.FromPath(path) + if err != nil { + p, err = layout.Write(path, empty.Index) + if err != nil { + return err + } + } + for ref, img := range imageMap { + opts := []layout.Option{} + if annotateRef { + parsed, err := name.ParseReference(ref, o.Name...) + if err != nil { + return err + } + opts = append(opts, layout.WithAnnotations(map[string]string{ + "org.opencontainers.image.ref.name": parsed.Name(), + })) + } + if err = p.AppendImage(img, opts...); err != nil { + return err + } + } + + for ref, idx := range indexMap { + opts := []layout.Option{} + if annotateRef { + parsed, err := name.ParseReference(ref, o.Name...) + if err != nil { + return err + } + opts = append(opts, layout.WithAnnotations(map[string]string{ + "org.opencontainers.image.ref.name": parsed.Name(), + })) + } + if err := p.AppendIndex(idx, opts...); err != nil { + return err + } + } + default: + return fmt.Errorf("unexpected --format: %q (valid values are: tarball, legacy, and oci)", format) + } + return nil + }, + } + cmd.Flags().StringVarP(&cachePath, "cache_path", "c", "", "Path to cache image layers") + cmd.Flags().StringVar(&format, "format", "tarball", fmt.Sprintf("Format in which to save images (%q, %q, or %q)", "tarball", "legacy", "oci")) + cmd.Flags().BoolVar(&annotateRef, "annotate-ref", false, "Preserves image reference used to pull as an annotation when used with --format=oci") + + return cmd +} diff --git a/cmd/crane/cmd/push.go b/cmd/crane/cmd/push.go new file mode 100644 index 0000000..887621e --- /dev/null +++ b/cmd/crane/cmd/push.go @@ -0,0 +1,126 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/cobra" +) + +// NewCmdPush creates a new cobra.Command for the push subcommand. +func NewCmdPush(options *[]crane.Option) *cobra.Command { + index := false + imageRefs := "" + cmd := &cobra.Command{ + Use: "push PATH IMAGE", + Short: "Push local image contents to a remote registry", + Long: `If the PATH is a directory, it will be read as an OCI image layout. Otherwise, PATH is assumed to be a docker-style tarball.`, + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + path, tag := args[0], args[1] + + img, err := loadImage(path, index) + if err != nil { + return err + } + + o := crane.GetOptions(*options...) + ref, err := name.ParseReference(tag, o.Name...) + if err != nil { + return err + } + var h v1.Hash + switch t := img.(type) { + case v1.Image: + if err := remote.Write(ref, t, o.Remote...); err != nil { + return err + } + if h, err = t.Digest(); err != nil { + return err + } + case v1.ImageIndex: + if err := remote.WriteIndex(ref, t, o.Remote...); err != nil { + return err + } + if h, err = t.Digest(); err != nil { + return err + } + default: + return fmt.Errorf("cannot push type (%T) to registry", img) + } + + digest := ref.Context().Digest(h.String()) + if imageRefs != "" { + return os.WriteFile(imageRefs, []byte(digest.String()), 0600) + } + // TODO(mattmoor): think about printing the digest to standard out + // to facilitate command composition similar to ko build. + + return nil + }, + } + cmd.Flags().BoolVar(&index, "index", false, "push a collection of images as a single index, currently required if PATH contains multiple images") + cmd.Flags().StringVar(&imageRefs, "image-refs", "", "path to file where a list of the published image references will be written") + return cmd +} + +func loadImage(path string, index bool) (partial.WithRawManifest, error) { + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !stat.IsDir() { + img, err := crane.Load(path) + if err != nil { + return nil, fmt.Errorf("loading %s as tarball: %w", path, err) + } + return img, nil + } + + l, err := layout.ImageIndexFromPath(path) + if err != nil { + return nil, fmt.Errorf("loading %s as OCI layout: %w", path, err) + } + + if index { + return l, nil + } + + m, err := l.IndexManifest() + if err != nil { + return nil, err + } + if len(m.Manifests) != 1 { + return nil, fmt.Errorf("layout contains %d entries, consider --index", len(m.Manifests)) + } + + desc := m.Manifests[0] + if desc.MediaType.IsImage() { + return l.Image(desc.Digest) + } else if desc.MediaType.IsIndex() { + return l.ImageIndex(desc.Digest) + } + + return nil, fmt.Errorf("layout contains non-image (mediaType: %q), consider --index", desc.MediaType) +} diff --git a/cmd/crane/cmd/rebase.go b/cmd/crane/cmd/rebase.go new file mode 100644 index 0000000..43f21b5 --- /dev/null +++ b/cmd/crane/cmd/rebase.go @@ -0,0 +1,210 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + "log" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + specsv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/spf13/cobra" +) + +// NewCmdRebase creates a new cobra.Command for the rebase subcommand. +func NewCmdRebase(options *[]crane.Option) *cobra.Command { + var orig, oldBase, newBase, rebased string + + rebaseCmd := &cobra.Command{ + Use: "rebase", + Short: "Rebase an image onto a new base image", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if orig == "" { + orig = args[0] + } else if len(args) != 0 || args[0] != "" { + return fmt.Errorf("cannot use --original with positional argument") + } + + // If the new ref isn't provided, write over the original image. + // If that ref was provided by digest (e.g., output from + // another crane command), then strip that and push the + // mutated image by digest instead. + if rebased == "" { + rebased = orig + } + + // Stupid hack to support insecure flag. + nameOpt := []name.Option{} + if ok, err := cmd.Parent().PersistentFlags().GetBool("insecure"); err != nil { + log.Fatalf("flag problems: %v", err) + } else if ok { + nameOpt = append(nameOpt, name.Insecure) + } + r, err := name.ParseReference(rebased, nameOpt...) + if err != nil { + log.Fatalf("parsing %s: %v", rebased, err) + } + + desc, err := crane.Head(orig, *options...) + if err != nil { + log.Fatalf("checking %s: %v", orig, err) + } + if !cmd.Parent().PersistentFlags().Changed("platform") && desc.MediaType.IsIndex() { + log.Fatalf("rebasing an index is not yet supported") + } + + origImg, err := crane.Pull(orig, *options...) + if err != nil { + return err + } + origMf, err := origImg.Manifest() + if err != nil { + return err + } + anns := origMf.Annotations + if newBase == "" && anns != nil { + newBase = anns[specsv1.AnnotationBaseImageName] + } + if newBase == "" { + return errors.New("could not determine new base image from annotations") + } + newBaseRef, err := name.ParseReference(newBase) + if err != nil { + return err + } + if oldBase == "" && anns != nil { + oldBaseDigest := anns[specsv1.AnnotationBaseImageDigest] + oldBase = newBaseRef.Context().Digest(oldBaseDigest).String() + } + if oldBase == "" { + return errors.New("could not determine old base image by digest from annotations") + } + + rebasedImg, err := rebaseImage(origImg, oldBase, newBase, *options...) + if err != nil { + return fmt.Errorf("rebasing image: %w", err) + } + + rebasedDigest, err := rebasedImg.Digest() + if err != nil { + return fmt.Errorf("digesting new image: %w", err) + } + origDigest, err := origImg.Digest() + if err != nil { + return err + } + if rebasedDigest == origDigest { + logs.Warn.Println("rebasing was no-op") + } + + if _, ok := r.(name.Digest); ok { + rebased = r.Context().Digest(rebasedDigest.String()).String() + } + logs.Progress.Println("pushing rebased image as", rebased) + if err := crane.Push(rebasedImg, rebased, *options...); err != nil { + log.Fatalf("pushing %s: %v", rebased, err) + } + + fmt.Println(r.Context().Digest(rebasedDigest.String())) + return nil + }, + } + rebaseCmd.Flags().StringVar(&orig, "original", "", "Original image to rebase (DEPRECATED: use positional arg instead)") + rebaseCmd.Flags().StringVar(&oldBase, "old_base", "", "Old base image to remove") + rebaseCmd.Flags().StringVar(&newBase, "new_base", "", "New base image to insert") + rebaseCmd.Flags().StringVar(&rebased, "rebased", "", "Tag to apply to rebased image (DEPRECATED: use --tag)") + rebaseCmd.Flags().StringVarP(&rebased, "tag", "t", "", "Tag to apply to rebased image") + return rebaseCmd +} + +// rebaseImage parses the references and uses them to perform a rebase on the +// original image. +// +// If oldBase or newBase are "", rebaseImage attempts to derive them using +// annotations in the original image. If those annotations are not found, +// rebaseImage returns an error. +// +// If rebasing is successful, base image annotations are set on the resulting +// image to facilitate implicit rebasing next time. +func rebaseImage(orig v1.Image, oldBase, newBase string, opt ...crane.Option) (v1.Image, error) { + m, err := orig.Manifest() + if err != nil { + return nil, err + } + if newBase == "" && m.Annotations != nil { + newBase = m.Annotations[specsv1.AnnotationBaseImageName] + if newBase != "" { + logs.Debug.Printf("Detected new base from %q annotation: %s", specsv1.AnnotationBaseImageName, newBase) + } + } + if newBase == "" { + return nil, fmt.Errorf("either new base or %q annotation is required", specsv1.AnnotationBaseImageName) + } + newBaseImg, err := crane.Pull(newBase, opt...) + if err != nil { + return nil, err + } + + if oldBase == "" && m.Annotations != nil { + oldBase = m.Annotations[specsv1.AnnotationBaseImageDigest] + if oldBase != "" { + newBaseRef, err := name.ParseReference(newBase) + if err != nil { + return nil, err + } + + oldBase = newBaseRef.Context().Digest(oldBase).String() + logs.Debug.Printf("Detected old base from %q annotation: %s", specsv1.AnnotationBaseImageDigest, oldBase) + } + } + if oldBase == "" { + return nil, fmt.Errorf("either old base or %q annotation is required", specsv1.AnnotationBaseImageDigest) + } + + oldBaseImg, err := crane.Pull(oldBase, opt...) + if err != nil { + return nil, err + } + + // NB: if newBase is an index, we need to grab the index's digest to + // annotate the resulting image, even though we pull the + // platform-specific image to rebase. + // crane.Digest will pull a platform-specific image, so use crane.Head + // here instead. + newBaseDesc, err := crane.Head(newBase, opt...) + if err != nil { + return nil, err + } + newBaseDigest := newBaseDesc.Digest.String() + + rebased, err := mutate.Rebase(orig, oldBaseImg, newBaseImg) + if err != nil { + return nil, err + } + + // Update base image annotations for the new image manifest. + logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageDigest, newBaseDigest) + logs.Debug.Printf("Setting annotation %q: %q", specsv1.AnnotationBaseImageName, newBase) + return mutate.Annotations(rebased, map[string]string{ + specsv1.AnnotationBaseImageDigest: newBaseDigest, + specsv1.AnnotationBaseImageName: newBase, + }).(v1.Image), nil +} diff --git a/cmd/crane/cmd/root.go b/cmd/crane/cmd/root.go new file mode 100644 index 0000000..95412cc --- /dev/null +++ b/cmd/crane/cmd/root.go @@ -0,0 +1,148 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "crypto/tls" + "fmt" + "net/http" + "os" + "path/filepath" + + "github.com/docker/cli/cli/config" + "github.com/google/go-containerregistry/internal/cmd" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/spf13/cobra" +) + +const ( + use = "crane" + short = "Crane is a tool for managing container images" +) + +var Root = New(use, short, []crane.Option{}) + +// New returns a top-level command for crane. This is mostly exposed +// to share code with gcrane. +func New(use, short string, options []crane.Option) *cobra.Command { + verbose := false + insecure := false + ndlayers := false + platform := &platformValue{} + + root := &cobra.Command{ + Use: use, + Short: short, + RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Usage() }, + DisableAutoGenTag: true, + SilenceUsage: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + options = append(options, crane.WithContext(cmd.Context())) + // TODO(jonjohnsonjr): crane.Verbose option? + if verbose { + logs.Debug.SetOutput(os.Stderr) + } + if insecure { + options = append(options, crane.Insecure) + } + if ndlayers { + options = append(options, crane.WithNondistributable()) + } + if Version != "" { + binary := "crane" + if len(os.Args[0]) != 0 { + binary = filepath.Base(os.Args[0]) + } + options = append(options, crane.WithUserAgent(fmt.Sprintf("%s/%s", binary, Version))) + } + + options = append(options, crane.WithPlatform(platform.platform)) + + transport := remote.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: insecure, //nolint: gosec + } + + var rt http.RoundTripper = transport + + // Add any http headers if they are set in the config file. + cf, err := config.Load(os.Getenv("DOCKER_CONFIG")) + if err != nil { + logs.Debug.Printf("failed to read config file: %v", err) + } else if len(cf.HTTPHeaders) != 0 { + rt = &headerTransport{ + inner: rt, + httpHeaders: cf.HTTPHeaders, + } + } + + options = append(options, crane.WithTransport(rt)) + }, + } + + root.AddCommand( + NewCmdAppend(&options), + NewCmdAuth(options, "crane", "auth"), + NewCmdBlob(&options), + NewCmdCatalog(&options, "crane"), + NewCmdConfig(&options), + NewCmdCopy(&options), + NewCmdDelete(&options), + NewCmdDigest(&options), + cmd.NewCmdEdit(&options), + NewCmdExport(&options), + NewCmdFlatten(&options), + NewCmdIndex(&options), + NewCmdList(&options), + NewCmdManifest(&options), + NewCmdMutate(&options), + NewCmdOptimize(&options), + NewCmdPull(&options), + NewCmdPush(&options), + NewCmdRebase(&options), + NewCmdTag(&options), + NewCmdValidate(&options), + NewCmdVersion(), + newCmdRegistry(), + ) + + root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable debug logs") + root.PersistentFlags().BoolVar(&insecure, "insecure", false, "Allow image references to be fetched without TLS") + root.PersistentFlags().BoolVar(&ndlayers, "allow-nondistributable-artifacts", false, "Allow pushing non-distributable (foreign) layers") + root.PersistentFlags().Var(platform, "platform", "Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64).") + + return root +} + +// headerTransport sets headers on outgoing requests. +type headerTransport struct { + httpHeaders map[string]string + inner http.RoundTripper +} + +// RoundTrip implements http.RoundTripper. +func (ht *headerTransport) RoundTrip(in *http.Request) (*http.Response, error) { + for k, v := range ht.httpHeaders { + if http.CanonicalHeaderKey(k) == "User-Agent" { + // Docker sets this, which is annoying, since we're not docker. + // We might want to revisit completely ignoring this. + continue + } + in.Header.Set(k, v) + } + return ht.inner.RoundTrip(in) +} diff --git a/cmd/crane/cmd/serve.go b/cmd/crane/cmd/serve.go new file mode 100644 index 0000000..5b11153 --- /dev/null +++ b/cmd/crane/cmd/serve.go @@ -0,0 +1,84 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "errors" + "fmt" + "log" + "net" + "net/http" + "os" + "time" + + "github.com/spf13/cobra" + + "github.com/google/go-containerregistry/pkg/registry" +) + +func newCmdRegistry() *cobra.Command { + cmd := &cobra.Command{ + Use: "registry", + } + cmd.AddCommand(newCmdServe()) + return cmd +} + +func newCmdServe() *cobra.Command { + return &cobra.Command{ + Use: "serve", + Short: "Serve an in-memory registry implementation", + Long: `This sub-command serves an in-memory registry implementation on port :8080 (or $PORT) + +The command blocks while the server accepts pushes and pulls. + +Contents are only stored in memory, and when the process exits, pushed data is lost.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + port := os.Getenv("PORT") + if port == "" { + port = "0" + } + listener, err := net.Listen("tcp", "localhost:"+port) + if err != nil { + log.Fatalln(err) + } + porti := listener.Addr().(*net.TCPAddr).Port + port = fmt.Sprintf("%d", porti) + + s := &http.Server{ + ReadHeaderTimeout: 5 * time.Second, // prevent slowloris, quiet linter + Handler: registry.New(), + } + log.Printf("serving on port %s", port) + + errCh := make(chan error) + go func() { errCh <- s.Serve(listener) }() + + <-ctx.Done() + log.Println("shutting down...") + if err := s.Shutdown(ctx); err != nil { + return err + } + + if err := <-errCh; !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + }, + } +} diff --git a/cmd/crane/cmd/tag.go b/cmd/crane/cmd/tag.go new file mode 100644 index 0000000..9af803a --- /dev/null +++ b/cmd/crane/cmd/tag.go @@ -0,0 +1,44 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/google/go-containerregistry/pkg/crane" + "github.com/spf13/cobra" +) + +// NewCmdTag creates a new cobra.Command for the tag subcommand. +func NewCmdTag(options *[]crane.Option) *cobra.Command { + return &cobra.Command{ + Use: "tag IMG TAG", + Short: "Efficiently tag a remote image", + Long: `This differs slightly from the "copy" command in a couple subtle ways: + +1. You don't have to specify the entire repository for the tag you're adding. For example, these two commands are functionally equivalent: +` + "```" + ` +crane cp registry.example.com/library/ubuntu:v0 registry.example.com/library/ubuntu:v1 +crane tag registry.example.com/library/ubuntu:v0 v1 +` + "```" + ` + +2. We can skip layer existence checks because we know the manifest already exists. This makes "tag" slightly faster than "copy".`, + Example: `# Add a v1 tag to ubuntu +crane tag ubuntu v1`, + Args: cobra.ExactArgs(2), + RunE: func(_ *cobra.Command, args []string) error { + img, tag := args[0], args[1] + return crane.Tag(img, tag, *options...) + }, + } +} diff --git a/cmd/crane/cmd/util.go b/cmd/crane/cmd/util.go new file mode 100644 index 0000000..f4ff651 --- /dev/null +++ b/cmd/crane/cmd/util.go @@ -0,0 +1,86 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +type platformsValue struct { + platforms []v1.Platform +} + +func (ps *platformsValue) Set(platform string) error { + if ps.platforms == nil { + ps.platforms = []v1.Platform{} + } + p, err := parsePlatform(platform) + if err != nil { + return err + } + pv := platformValue{p} + ps.platforms = append(ps.platforms, *pv.platform) + return nil +} + +func (ps *platformsValue) String() string { + ss := make([]string, 0, len(ps.platforms)) + for _, p := range ps.platforms { + ss = append(ss, p.String()) + } + return strings.Join(ss, ",") +} + +func (ps *platformsValue) Type() string { + return "platform(s)" +} + +type platformValue struct { + platform *v1.Platform +} + +func (pv *platformValue) Set(platform string) error { + p, err := parsePlatform(platform) + if err != nil { + return err + } + pv.platform = p + return nil +} + +func (pv *platformValue) String() string { + return platformToString(pv.platform) +} + +func (pv *platformValue) Type() string { + return "platform" +} + +func platformToString(p *v1.Platform) string { + if p == nil { + return "all" + } + return p.String() +} + +func parsePlatform(platform string) (*v1.Platform, error) { + if platform == "all" { + return nil, nil + } + + return v1.ParsePlatform(platform) +} diff --git a/cmd/crane/cmd/validate.go b/cmd/crane/cmd/validate.go new file mode 100644 index 0000000..4a4acbd --- /dev/null +++ b/cmd/crane/cmd/validate.go @@ -0,0 +1,73 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/validate" + "github.com/spf13/cobra" +) + +// NewCmdValidate creates a new cobra.Command for the validate subcommand. +func NewCmdValidate(options *[]crane.Option) *cobra.Command { + var ( + tarballPath, remoteRef string + fast bool + ) + + validateCmd := &cobra.Command{ + Use: "validate", + Short: "Validate that an image is well-formed", + Args: cobra.ExactArgs(0), + RunE: func(_ *cobra.Command, args []string) error { + for flag, maker := range map[string]func(string, ...crane.Option) (v1.Image, error){ + tarballPath: makeTarball, + remoteRef: crane.Pull, + } { + if flag == "" { + continue + } + img, err := maker(flag, *options...) + if err != nil { + return fmt.Errorf("failed to read image %s: %w", flag, err) + } + + opt := []validate.Option{} + if fast { + opt = append(opt, validate.Fast) + } + if err := validate.Image(img, opt...); err != nil { + fmt.Printf("FAIL: %s: %v\n", flag, err) + return err + } + fmt.Printf("PASS: %s\n", flag) + } + return nil + }, + } + validateCmd.Flags().StringVar(&tarballPath, "tarball", "", "Path to tarball to validate") + validateCmd.Flags().StringVar(&remoteRef, "remote", "", "Name of remote image to validate") + validateCmd.Flags().BoolVar(&fast, "fast", false, "Skip downloading/digesting layers") + + return validateCmd +} + +func makeTarball(path string, _ ...crane.Option) (v1.Image, error) { + return tarball.ImageFromPath(path, nil) +} diff --git a/cmd/crane/cmd/version.go b/cmd/crane/cmd/version.go new file mode 100644 index 0000000..b906a5d --- /dev/null +++ b/cmd/crane/cmd/version.go @@ -0,0 +1,56 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "runtime/debug" + + "github.com/spf13/cobra" +) + +// Version can be set via: +// -ldflags="-X 'github.com/google/go-containerregistry/cmd/crane/cmd.Version=$TAG'" +var Version string + +func init() { + if Version == "" { + i, ok := debug.ReadBuildInfo() + if !ok { + return + } + Version = i.Main.Version + } +} + +// NewCmdVersion creates a new cobra.Command for the version subcommand. +func NewCmdVersion() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version", + Long: `The version string is completely dependent on how the binary was built, so you should not depend on the version format. It may change without notice. + +This could be an arbitrary string, if specified via -ldflags. +This could also be the go module version, if built with go modules (often "(devel)").`, + Args: cobra.NoArgs, + Run: func(_ *cobra.Command, _ []string) { + if Version == "" { + fmt.Println("could not determine build information") + } else { + fmt.Println(Version) + } + }, + } +} diff --git a/cmd/crane/depcheck_test.go b/cmd/crane/depcheck_test.go new file mode 100644 index 0000000..f36056e --- /dev/null +++ b/cmd/crane/depcheck_test.go @@ -0,0 +1,32 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" + + "github.com/google/go-containerregistry/internal/depcheck" +) + +func TestDeps(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow depcheck") + } + depcheck.AssertNoDependency(t, map[string][]string{ + "github.com/google/go-containerregistry/cmd/crane": { + "github.com/google/go-containerregistry/pkg/v1/daemon", + }, + }) +} diff --git a/cmd/crane/doc/crane.md b/cmd/crane/doc/crane.md new file mode 100644 index 0000000..afd1b24 --- /dev/null +++ b/cmd/crane/doc/crane.md @@ -0,0 +1,42 @@ +## crane + +Crane is a tool for managing container images + +``` +crane [flags] +``` + +### Options + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + -h, --help help for crane + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane append](crane_append.md) - Append contents of a tarball to a remote image +* [crane auth](crane_auth.md) - Log in or access credentials +* [crane blob](crane_blob.md) - Read a blob from the registry +* [crane catalog](crane_catalog.md) - List the repos in a registry +* [crane config](crane_config.md) - Get the config of an image +* [crane copy](crane_copy.md) - Efficiently copy a remote image from src to dst while retaining the digest value +* [crane delete](crane_delete.md) - Delete an image reference from its registry +* [crane digest](crane_digest.md) - Get the digest of an image +* [crane export](crane_export.md) - Export filesystem of a container image as a tarball +* [crane flatten](crane_flatten.md) - Flatten an image's layers into a single layer +* [crane index](crane_index.md) - Modify an image index. +* [crane ls](crane_ls.md) - List the tags in a repo +* [crane manifest](crane_manifest.md) - Get the manifest of an image +* [crane mutate](crane_mutate.md) - Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there. +* [crane pull](crane_pull.md) - Pull remote images by reference and store their contents locally +* [crane push](crane_push.md) - Push local image contents to a remote registry +* [crane rebase](crane_rebase.md) - Rebase an image onto a new base image +* [crane registry](crane_registry.md) - +* [crane tag](crane_tag.md) - Efficiently tag a remote image +* [crane validate](crane_validate.md) - Validate that an image is well-formed +* [crane version](crane_version.md) - Print the version + diff --git a/cmd/crane/doc/crane_append.md b/cmd/crane/doc/crane_append.md new file mode 100644 index 0000000..d637dd1 --- /dev/null +++ b/cmd/crane/doc/crane_append.md @@ -0,0 +1,43 @@ +## crane append + +Append contents of a tarball to a remote image + +### Synopsis + +This sub-command pushes an image based on an (optional) +base image, with appended layers containing the contents of the +provided tarballs. + +If the base image is a Windows base image (i.e., its config.OS is "windows"), +the contents of the tarballs will be modified to be suitable for a Windows +container image. + +``` +crane append [flags] +``` + +### Options + +``` + -b, --base string Name of base image to append to + -h, --help help for append + -f, --new_layer strings Path to tarball to append to image + -t, --new_tag string Tag to apply to resulting image + --oci-empty-base If true, empty base image will have OCI media types instead of Docker + -o, --output string Path to new tarball of resulting image + --set-base-image-annotations If true, annotate the resulting image as being based on the base image +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_auth.md b/cmd/crane/doc/crane_auth.md new file mode 100644 index 0000000..6eb8fc8 --- /dev/null +++ b/cmd/crane/doc/crane_auth.md @@ -0,0 +1,29 @@ +## crane auth + +Log in or access credentials + +``` +crane auth [flags] +``` + +### Options + +``` + -h, --help help for auth +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images +* [crane auth get](crane_auth_get.md) - Implements a credential helper +* [crane auth login](crane_auth_login.md) - Log in to a registry + diff --git a/cmd/crane/doc/crane_auth_get.md b/cmd/crane/doc/crane_auth_get.md new file mode 100644 index 0000000..6ff89c1 --- /dev/null +++ b/cmd/crane/doc/crane_auth_get.md @@ -0,0 +1,38 @@ +## crane auth get + +Implements a credential helper + +``` +crane auth get [REGISTRY_ADDR] [flags] +``` + +### Examples + +``` + # Read configured credentials for reg.example.com + $ echo "reg.example.com" | crane auth get + {"username":"AzureDiamond","password":"hunter2"} + # or + $ crane auth get reg.example.com + {"username":"AzureDiamond","password":"hunter2"} +``` + +### Options + +``` + -h, --help help for get +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane auth](crane_auth.md) - Log in or access credentials + diff --git a/cmd/crane/doc/crane_auth_login.md b/cmd/crane/doc/crane_auth_login.md new file mode 100644 index 0000000..1fec423 --- /dev/null +++ b/cmd/crane/doc/crane_auth_login.md @@ -0,0 +1,37 @@ +## crane auth login + +Log in to a registry + +``` +crane auth login [OPTIONS] [SERVER] [flags] +``` + +### Examples + +``` + # Log in to reg.example.com + crane auth login reg.example.com -u AzureDiamond -p hunter2 +``` + +### Options + +``` + -h, --help help for login + -p, --password string Password + --password-stdin Take the password from stdin + -u, --username string Username +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane auth](crane_auth.md) - Log in or access credentials + diff --git a/cmd/crane/doc/crane_blob.md b/cmd/crane/doc/crane_blob.md new file mode 100644 index 0000000..36f615a --- /dev/null +++ b/cmd/crane/doc/crane_blob.md @@ -0,0 +1,33 @@ +## crane blob + +Read a blob from the registry + +``` +crane blob BLOB [flags] +``` + +### Examples + +``` +crane blob ubuntu@sha256:4c1d20cdee96111c8acf1858b62655a37ce81ae48648993542b7ac363ac5c0e5 > blob.tar.gz +``` + +### Options + +``` + -h, --help help for blob +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_catalog.md b/cmd/crane/doc/crane_catalog.md new file mode 100644 index 0000000..99b81ad --- /dev/null +++ b/cmd/crane/doc/crane_catalog.md @@ -0,0 +1,34 @@ +## crane catalog + +List the repos in a registry + +``` +crane catalog [REGISTRY] [flags] +``` + +### Examples + +``` + # list the repos for reg.example.com + $ crane catalog reg.example.com +``` + +### Options + +``` + -h, --help help for catalog +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_config.md b/cmd/crane/doc/crane_config.md new file mode 100644 index 0000000..5d7fa5a --- /dev/null +++ b/cmd/crane/doc/crane_config.md @@ -0,0 +1,27 @@ +## crane config + +Get the config of an image + +``` +crane config IMAGE [flags] +``` + +### Options + +``` + -h, --help help for config +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_copy.md b/cmd/crane/doc/crane_copy.md new file mode 100644 index 0000000..8e7e1a8 --- /dev/null +++ b/cmd/crane/doc/crane_copy.md @@ -0,0 +1,27 @@ +## crane copy + +Efficiently copy a remote image from src to dst while retaining the digest value + +``` +crane copy SRC DST [flags] +``` + +### Options + +``` + -h, --help help for copy +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_delete.md b/cmd/crane/doc/crane_delete.md new file mode 100644 index 0000000..7932ea2 --- /dev/null +++ b/cmd/crane/doc/crane_delete.md @@ -0,0 +1,27 @@ +## crane delete + +Delete an image reference from its registry + +``` +crane delete IMAGE [flags] +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_digest.md b/cmd/crane/doc/crane_digest.md new file mode 100644 index 0000000..f141b36 --- /dev/null +++ b/cmd/crane/doc/crane_digest.md @@ -0,0 +1,29 @@ +## crane digest + +Get the digest of an image + +``` +crane digest IMAGE [flags] +``` + +### Options + +``` + --full-ref (Optional) if true, print the full image reference by digest + -h, --help help for digest + --tarball string (Optional) path to tarball containing the image +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_export.md b/cmd/crane/doc/crane_export.md new file mode 100644 index 0000000..ca10c56 --- /dev/null +++ b/cmd/crane/doc/crane_export.md @@ -0,0 +1,40 @@ +## crane export + +Export filesystem of a container image as a tarball + +``` +crane export IMAGE|- TARBALL|- [flags] +``` + +### Examples + +``` + # Write tarball to stdout + crane export ubuntu - + + # Write tarball to file + crane export ubuntu ubuntu.tar + + # Read image from stdin + crane export - ubuntu.tar +``` + +### Options + +``` + -h, --help help for export +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_flatten.md b/cmd/crane/doc/crane_flatten.md new file mode 100644 index 0000000..68e6bc6 --- /dev/null +++ b/cmd/crane/doc/crane_flatten.md @@ -0,0 +1,28 @@ +## crane flatten + +Flatten an image's layers into a single layer + +``` +crane flatten [flags] +``` + +### Options + +``` + -h, --help help for flatten + -t, --tag string New tag to apply to flattened image. If not provided, push by digest to the original image repository. +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_index.md b/cmd/crane/doc/crane_index.md new file mode 100644 index 0000000..2adea48 --- /dev/null +++ b/cmd/crane/doc/crane_index.md @@ -0,0 +1,29 @@ +## crane index + +Modify an image index. + +``` +crane index [flags] +``` + +### Options + +``` + -h, --help help for index +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images +* [crane index append](crane_index_append.md) - Append manifests to a remote index. +* [crane index filter](crane_index_filter.md) - Modifies a remote index by filtering based on platform. + diff --git a/cmd/crane/doc/crane_index_append.md b/cmd/crane/doc/crane_index_append.md new file mode 100644 index 0000000..a6c1541 --- /dev/null +++ b/cmd/crane/doc/crane_index_append.md @@ -0,0 +1,47 @@ +## crane index append + +Append manifests to a remote index. + +### Synopsis + +This sub-command pushes an index based on an (optional) base index, with appended manifests. + +The platform for appended manifests is inferred from the config file or omitted if that is infeasible. + +``` +crane index append [flags] +``` + +### Examples + +``` + # Append a windows hello-world image to ubuntu, push to example.com/hello-world:weird + crane index append ubuntu -m hello-world@sha256:87b9ca29151260634b95efb84d43b05335dc3ed36cc132e2b920dd1955342d20 -t example.com/hello-world:weird + + # Create an index from scratch for etcd. + crane index append -m registry.k8s.io/etcd-amd64:3.4.9 -m registry.k8s.io/etcd-arm64:3.4.9 -t example.com/etcd +``` + +### Options + +``` + --docker-empty-base If true, empty base index will have Docker media types instead of OCI + --flatten If true, appending an index will append each of its children rather than the index itself (default true) + -h, --help help for append + -m, --manifest strings References to manifests to append to the base index + -t, --tag string Tag to apply to resulting image +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane index](crane_index.md) - Modify an image index. + diff --git a/cmd/crane/doc/crane_index_filter.md b/cmd/crane/doc/crane_index_filter.md new file mode 100644 index 0000000..bda1f8d --- /dev/null +++ b/cmd/crane/doc/crane_index_filter.md @@ -0,0 +1,41 @@ +## crane index filter + +Modifies a remote index by filtering based on platform. + +``` +crane index filter [flags] +``` + +### Examples + +``` + # Filter out weird platforms from ubuntu, copy result to example.com/ubuntu + crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t example.com/ubuntu + + # Filter out any non-linux platforms, push to example.com/hello-world + crane index filter hello-world --platform linux -t example.com/hello-world + + # Same as above, but in-place + crane index filter example.com/hello-world:some-tag --platform linux +``` + +### Options + +``` + -h, --help help for filter + --platform platform(s) Specifies the platform(s) to keep from base in the form os/arch[/variant][:osversion][,<platform>] (e.g. linux/amd64). + -t, --tag string Tag to apply to resulting image +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane index](crane_index.md) - Modify an image index. + diff --git a/cmd/crane/doc/crane_ls.md b/cmd/crane/doc/crane_ls.md new file mode 100644 index 0000000..6616820 --- /dev/null +++ b/cmd/crane/doc/crane_ls.md @@ -0,0 +1,29 @@ +## crane ls + +List the tags in a repo + +``` +crane ls REPO [flags] +``` + +### Options + +``` + --full-ref (Optional) if true, print the full image reference + -h, --help help for ls + --omit-digest-tags (Optional), if true, omit digest tags (e.g., ':sha256-...') +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_manifest.md b/cmd/crane/doc/crane_manifest.md new file mode 100644 index 0000000..3d61b4e --- /dev/null +++ b/cmd/crane/doc/crane_manifest.md @@ -0,0 +1,27 @@ +## crane manifest + +Get the manifest of an image + +``` +crane manifest IMAGE [flags] +``` + +### Options + +``` + -h, --help help for manifest +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_mutate.md b/cmd/crane/doc/crane_mutate.md new file mode 100644 index 0000000..0174b09 --- /dev/null +++ b/cmd/crane/doc/crane_mutate.md @@ -0,0 +1,37 @@ +## crane mutate + +Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there. + +``` +crane mutate [flags] +``` + +### Options + +``` + -a, --annotation stringToString New annotations to add (default []) + --append strings Path to tarball to append to image + --cmd strings New cmd to set + --entrypoint strings New entrypoint to set + -e, --env stringToString New envvar to add (default []) + -h, --help help for mutate + -l, --label stringToString New labels to add (default []) + -o, --output string Path to new tarball of resulting image + --repo string Repository to push the mutated image to. If provided, push by digest to this repository. + -t, --tag string New tag reference to apply to mutated image. If not provided, push by digest to the original image repository. + -u, --user string New user to set +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_pull.md b/cmd/crane/doc/crane_pull.md new file mode 100644 index 0000000..790a1cb --- /dev/null +++ b/cmd/crane/doc/crane_pull.md @@ -0,0 +1,30 @@ +## crane pull + +Pull remote images by reference and store their contents locally + +``` +crane pull IMAGE TARBALL [flags] +``` + +### Options + +``` + --annotate-ref Preserves image reference used to pull as an annotation when used with --format=oci + -c, --cache_path string Path to cache image layers + --format string Format in which to save images ("tarball", "legacy", or "oci") (default "tarball") + -h, --help help for pull +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_push.md b/cmd/crane/doc/crane_push.md new file mode 100644 index 0000000..64bacf6 --- /dev/null +++ b/cmd/crane/doc/crane_push.md @@ -0,0 +1,33 @@ +## crane push + +Push local image contents to a remote registry + +### Synopsis + +If the PATH is a directory, it will be read as an OCI image layout. Otherwise, PATH is assumed to be a docker-style tarball. + +``` +crane push PATH IMAGE [flags] +``` + +### Options + +``` + -h, --help help for push + --image-refs string path to file where a list of the published image references will be written + --index push a collection of images as a single index, currently required if PATH contains multiple images +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_rebase.md b/cmd/crane/doc/crane_rebase.md new file mode 100644 index 0000000..e30f078 --- /dev/null +++ b/cmd/crane/doc/crane_rebase.md @@ -0,0 +1,32 @@ +## crane rebase + +Rebase an image onto a new base image + +``` +crane rebase [flags] +``` + +### Options + +``` + -h, --help help for rebase + --new_base string New base image to insert + --old_base string Old base image to remove + --original string Original image to rebase (DEPRECATED: use positional arg instead) + --rebased string Tag to apply to rebased image (DEPRECATED: use --tag) + -t, --tag string Tag to apply to rebased image +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_registry.md b/cmd/crane/doc/crane_registry.md new file mode 100644 index 0000000..d2bf920 --- /dev/null +++ b/cmd/crane/doc/crane_registry.md @@ -0,0 +1,24 @@ +## crane registry + + + +### Options + +``` + -h, --help help for registry +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images +* [crane registry serve](crane_registry_serve.md) - Serve an in-memory registry implementation + diff --git a/cmd/crane/doc/crane_registry_serve.md b/cmd/crane/doc/crane_registry_serve.md new file mode 100644 index 0000000..6c46861 --- /dev/null +++ b/cmd/crane/doc/crane_registry_serve.md @@ -0,0 +1,35 @@ +## crane registry serve + +Serve an in-memory registry implementation + +### Synopsis + +This sub-command serves an in-memory registry implementation on port :8080 (or $PORT) + +The command blocks while the server accepts pushes and pulls. + +Contents are only stored in memory, and when the process exits, pushed data is lost. + +``` +crane registry serve [flags] +``` + +### Options + +``` + -h, --help help for serve +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane registry](crane_registry.md) - + diff --git a/cmd/crane/doc/crane_tag.md b/cmd/crane/doc/crane_tag.md new file mode 100644 index 0000000..5433467 --- /dev/null +++ b/cmd/crane/doc/crane_tag.md @@ -0,0 +1,46 @@ +## crane tag + +Efficiently tag a remote image + +### Synopsis + +This differs slightly from the "copy" command in a couple subtle ways: + +1. You don't have to specify the entire repository for the tag you're adding. For example, these two commands are functionally equivalent: +``` +crane cp registry.example.com/library/ubuntu:v0 registry.example.com/library/ubuntu:v1 +crane tag registry.example.com/library/ubuntu:v0 v1 +``` + +2. We can skip layer existence checks because we know the manifest already exists. This makes "tag" slightly faster than "copy". + +``` +crane tag IMG TAG [flags] +``` + +### Examples + +``` +# Add a v1 tag to ubuntu +crane tag ubuntu v1 +``` + +### Options + +``` + -h, --help help for tag +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_validate.md b/cmd/crane/doc/crane_validate.md new file mode 100644 index 0000000..cff22f8 --- /dev/null +++ b/cmd/crane/doc/crane_validate.md @@ -0,0 +1,30 @@ +## crane validate + +Validate that an image is well-formed + +``` +crane validate [flags] +``` + +### Options + +``` + --fast Skip downloading/digesting layers + -h, --help help for validate + --remote string Name of remote image to validate + --tarball string Path to tarball to validate +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/doc/crane_version.md b/cmd/crane/doc/crane_version.md new file mode 100644 index 0000000..0972792 --- /dev/null +++ b/cmd/crane/doc/crane_version.md @@ -0,0 +1,34 @@ +## crane version + +Print the version + +### Synopsis + +The version string is completely dependent on how the binary was built, so you should not depend on the version format. It may change without notice. + +This could be an arbitrary string, if specified via -ldflags. +This could also be the go module version, if built with go modules (often "(devel)"). + +``` +crane version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### Options inherited from parent commands + +``` + --allow-nondistributable-artifacts Allow pushing non-distributable (foreign) layers + --insecure Allow image references to be fetched without TLS + --platform platform Specifies the platform in the form os/arch[/variant][:osversion] (e.g. linux/amd64). (default all) + -v, --verbose Enable debug logs +``` + +### SEE ALSO + +* [crane](crane.md) - Crane is a tool for managing container images + diff --git a/cmd/crane/help/README.md b/cmd/crane/help/README.md new file mode 100644 index 0000000..c97606c --- /dev/null +++ b/cmd/crane/help/README.md @@ -0,0 +1,5 @@ +## Generate docs for `crane` + +```go +go run cmd/crane/help/main.go --dir=cmd/crane/doc/ +``` diff --git a/cmd/crane/help/main.go b/cmd/crane/help/main.go new file mode 100644 index 0000000..e086a55 --- /dev/null +++ b/cmd/crane/help/main.go @@ -0,0 +1,45 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + + "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" +) + +var dir string +var root = &cobra.Command{ + Use: "gendoc", + Short: "Generate crane's help docs", + Args: cobra.NoArgs, + RunE: func(*cobra.Command, []string) error { + return doc.GenMarkdownTree(cmd.Root, dir) + }, +} + +func init() { + root.Flags().StringVarP(&dir, "dir", "d", ".", "Path to directory in which to generate docs") +} + +func main() { + if err := root.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/crane/main.go b/cmd/crane/main.go new file mode 100644 index 0000000..6dbdf22 --- /dev/null +++ b/cmd/crane/main.go @@ -0,0 +1,38 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "os" + "os/signal" + + "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/google/go-containerregistry/pkg/logs" +) + +func init() { + logs.Warn.SetOutput(os.Stderr) + logs.Progress.SetOutput(os.Stderr) +} + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + if err := cmd.Root.ExecuteContext(ctx); err != nil { + cancel() + os.Exit(1) + } +} diff --git a/cmd/crane/rebase.md b/cmd/crane/rebase.md new file mode 100644 index 0000000..da4b255 --- /dev/null +++ b/cmd/crane/rebase.md @@ -0,0 +1,125 @@ +### This code is experimental and might break you if not used correctly. + +The `rebase` command efficiently rewrites an image to replace the base image it +is `FROM` with a new base image. + +![rebase visualization](./rebase.png) + +([link](https://docs.google.com/drawings/d/1w8UxTZDRbDWVoqnbr17SJuU73pRxpOmOk_vzmC9WB2k/edit)) + +**This is not safe in general**, but it can be extremely useful for platform +providers, e.g. when a vulnerability is discovered in a base layer and many +thousands or millions of applications need to be patched in a short period of +time. + +A commonly accepted guideline for rebase-safety is ABI-compatibility, but this +is still imperfect in a handful of ways, and the exact contract varies between +platform providers. + +Rebasing is best suited for when rebuilding is either impossible (source is not +available) or impractical (too much work, too little time). + +## Using `crane rebase` + +For purposes of illustration, imagine you've built a container image +`my-app:latest`, which is `FROM ubuntu`: + +``` +FROM ubuntu + +RUN ./very-expensive-build-process.sh + +ENTRYPOINT ["/bin/myapp"] +``` + +A serious vulnerability has been found in the `ubuntu` base image, and a new +patched version has been released, tagged as `ubuntu:latest`. + +You could build your app image again, and the Dockerfile's `FROM ubuntu` +directive would pick up the new base image release, but that requires a full +rebuild of your entire app from source, which might take a long time, and might +pull in other unrelated changes in dependencies. + +You may have thousands of images containing the vulnerability. You just want to +release this critical bug fix across all your apps, as quickly as possible. + +Instead, you could use `crane rebase` to replace the vulnerable base image +layers in your image with the patched base image layers, without requiring a +full rebuild from source. + +``` +$ crane rebase my-app:latest \ + --old_base=ubuntu@sha256:deadbeef... \ + --new_base=ubuntu:latest \ + --tag=my-app:rebased +``` + +This command: + +1. fetches the manifest for the original image `my-app:latest`, and the + `old_base` and `new_base` images +1. checks that the original image is indeed based on `old_base` +1. removes `old_base`'s layers from the original image +1. replaces them with `new_base`'s layers +1. computes and uploads a new manifest for the image, tagged as `--tag`. + +If `--tag` is not specified, its value will be assumed to be the original +image's name. If the original image was specified by digest, the resulting +image will be pushed by digest only. + +`crane rebase` will print the rebased image name by digest to `stdout`. + +### Base Image Annotation Hints + +The OCI image spec includes some [standard image +annotations](https://github.com/opencontainers/image-spec/blob/main/annotations.md) +that can provide hints for the `--old_base` and `--new_base` flag values, so +these don't need to be specified: + +- **`org.opencontainers.image.base.digest`** specifies the original digest of + the base image +- **`org.opencontainers.image.base.name`** specifies the original base image's + reference + +If the original image has these annotations, you can omit the `--old_base` and +`--new_base` flags, and their values will be assumed to be: + +- `--old_base`: the `base.name` annotation value, plus the `base.digest` + annotation value +- `--new_base`: the `base.name` annotation value + +If these annotation values are invalid, and the flags aren't set, the operation +will fail. + +Whether or not the annotation values were set on the original image, they +_will_ be set on the resulting rebased image, to ease future rebase operations +on that image. + +`crane append` also supports the `--set-base-image-annotations` flag, which, if +true, will set these annotations on the resulting image. + +## Caveats + +The tool has no visibility into what the specific contents of the resulting +image, and has no idea what constitutes a "valid" image. As a result, it's +perfectly capable of producing an image that's entirely invalid garbage. +Rebasing arbitrary layers in an image is not a good idea. + +To help prevent garbage images, rebasing should only be done at a point in the +layer stack between "base" layers and "app" layers. These should adhere to some +contract about what "base" layers can be expected to produce, and what "app" +layers should expect from base layers. + +In the example above, for instance, we assume that the Ubuntu base image is +adhering to some contract with downstream app layers, that it won't remove or +drastically change what it provides to the app layer. If the `new_base` layers +removed some installed package, or made a breaking change to the version of +some compiler expected by the uppermost app layers, the resulting rebased image +might be invalid. + +In general, it's a good practice to tag rebased images to some other tag than +the `original` tag, perform some sanity checks, then tag the image to the +`original` tag once it's determined the image is valid. + +There is ongoing work to standardize and advertise base image contract +adherence to make rebasing safer. diff --git a/cmd/crane/rebase.png b/cmd/crane/rebase.png Binary files differnew file mode 100644 index 0000000..449bdfe --- /dev/null +++ b/cmd/crane/rebase.png diff --git a/cmd/crane/rebase_test.sh b/cmd/crane/rebase_test.sh new file mode 100755 index 0000000..727062b --- /dev/null +++ b/cmd/crane/rebase_test.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -ex + +tmp=$(mktemp -d) + +go install ./cmd/registry +go build -o ./crane ./cmd/crane + +# Start a local registry. +registry & +PID=$! +function cleanup { + kill $PID + rm -r ${tmp} + rm ./crane +} +trap cleanup EXIT + +sleep 1 # Wait for registry to be up. + +# Create an image localhost:1338/base containing a.txt +echo a > ${tmp}/a.txt +old_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base) +rm ${tmp}/a.txt + +# Append to that image localhost:1338/rebaseme +echo top > ${tmp}/top.txt +orig=$(./crane append -f <(tar -f - -c ${tmp}) -b ${old_base} -t localhost:1338/rebaseme) +rm ${tmp}/top.txt + +# Annotate that image as the base image (by ref and digest) +# TODO: do this with a flag to --append +orig=$(./crane mutate ${orig} \ + --annotation org.opencontainers.image.base.name=localhost:1338/base \ + --annotation org.opencontainers.image.base.digest=$(./crane digest localhost:1338/base)) + +# Update localhost:1338/base containing b.txt +echo b > ${tmp}/b.txt +new_base=$(./crane append -f <(tar -f - -c ${tmp}) -t localhost:1338/base) +rm ${tmp}/b.txt + +# Rebase using annotations +rebased=$(./crane rebase ${orig}) + +# List files in the rebased image. +./crane export ${rebased} - | tar -tvf - + +# Extract b.txt out of the rebased image. +./crane export ${rebased} - | tar -Oxf - ${tmp:1}/b.txt + +# Extract top.txt out of the rebased image. +./crane export ${rebased} - | tar -Oxf - ${tmp:1}/top.txt + +# a.txt is _not_ in the rebased image. +set +e +./crane export ${rebased} - | tar -Oxf - ${tmp:1}/a.txt # this should fail +code=$? +echo "finding a.txt exited ${code}" +if [[ $code -eq 0 ]]; then + echo "a.txt was found in rebased image" + exit 1 +fi diff --git a/cmd/crane/recipes.md b/cmd/crane/recipes.md new file mode 100644 index 0000000..1c0121d --- /dev/null +++ b/cmd/crane/recipes.md @@ -0,0 +1,105 @@ +# `crane` Recipes + +Useful tips and things you can do with `crane` and other standard tools. + +### List files in an image + +``` +crane export ubuntu - | tar -tvf - | less +``` + +### Extract a single file from an image + +``` +crane export ubuntu - | tar -Oxf - etc/passwd +``` + +Note: Be sure to remove the leading `/` from the path (not `/etc/passwd`). This behavior will not follow symlinks. + +### Bundle directory contents into an image + +``` +crane append -f <(tar -f - -c some-dir/) -t ${IMAGE} +``` + +By default, this produces an image with one layer containing the directory contents. Add `-b ${BASE_IMAGE}` to append the layer to a base image instead. + +You can extend this even further with `crane mutate`, to make an executable in the appended layer the image's entrypoint. + +``` +crane mutate ${IMAGE} --entrypoint=some-dir/entrypoint.sh +``` + +Because `crane append` emits the full image reference, these calls can even be chained together: + +``` +crane mutate $( + crane append -f <(tar -f - -c some-dir/) -t ${IMAGE} +) --entrypoint=some-dir/entrypoint.sh +``` + +This will bundle `some-dir/` into an image, push it, mutate its entrypoint to `some-dir/entrypoint.sh`, and push that new image by digest. + +### Diff two configs + +``` +diff <(crane config busybox:1.32 | jq) <(crane config busybox:1.33 | jq) +``` + +### Diff two manifests + +``` +diff <(crane manifest busybox:1.32 | jq) <(crane manifest busybox:1.33 | jq) +``` + +### Diff filesystem contents + +``` +diff \ + <(crane export gcr.io/kaniko-project/executor:v1.6.0-debug - | tar -tvf - | sort) \ + <(crane export gcr.io/kaniko-project/executor:v1.7.0-debug - | tar -tvf - | sort) +``` + +This will show file size diffs and (unfortunately) modified time diffs. + +With some work, you can use `cut` and other built-in Unix tools to ignore these diffs. + +### Get total image size + +Given an image manifest, you can calculate the total size of all layer blobs and the image's config blob using `jq`: + +``` +crane manifest gcr.io/buildpacks/builder:v1 | jq '.config.size + ([.layers[].size] | add)' +``` + +This will produce a number of bytes, which you can make human-readable by passing to [`numfmt`](https://www.gnu.org/software/coreutils/manual/html_node/numfmt-invocation.html) + +``` +crane manifest gcr.io/buildpacks/builder:v1 | jq '.config.size + ([.layers[].size] | add)' | numfmt --to=iec +``` + +For image indexes, you can pass the `--platform` flag to `crane` to get a platform-specific image. + +### Filter irrelevant platforms from a multi-platform image + +Perhaps you use a base image that supports a wide variety of exotic platforms, but you only care about linux/amd64 and linux/arm64. +If you want to copy that base image into a different registry, you will end up with a bunch of images you don't use. +You can filter the base to include only platforms that are relevant to you. + +``` +crane index filter ubuntu --platform linux/amd64 --platform linux/arm64 -t ${IMAGE} +``` + +Note that this will obviously modify the digest of the multi-platform image you're using, so this may invalidate other artifacts that reference it, e.g. signatures. + +### Create a multi-platform image from scratch + +If you have a bunch of platform-specific images that you want to turn into a multi-platform image, `crane index append` can do that: + +``` +crane index append -t ${IMAGE} \ + -m ubuntu@sha256:c985bc3f77946b8e92c9a3648c6f31751a7dd972e06604785e47303f4ad47c4c \ + -m ubuntu@sha256:61bd0b97000996232eb07b8d0e9375d14197f78aa850c2506417ef995a7199a7 +``` + +Note that this is less flexible than [`manifest-tool`](https://github.com/estesp/manifest-tool) because it derives the platform from each image's config file, but it should work in most cases. diff --git a/cmd/gcrane/README.md b/cmd/gcrane/README.md new file mode 100644 index 0000000..c8c9ba2 --- /dev/null +++ b/cmd/gcrane/README.md @@ -0,0 +1,65 @@ +# `gcrane` + +<img src="../../images/gcrane.png" width="40%"> + +This tool implements a superset of the [`crane`](../crane/README.md) commands, with +additional commands that are specific to [gcr.io](https://gcr.io). + +Note that this relies on some implementation details of GCR that are not +consistent with the [registry spec](https://docs.docker.com/registry/spec/api/), +so this may break in the future. + +## Installation + +Download [latest release](https://github.com/google/go-containerregistry/releases/latest). + +Install manually: + +``` +go install github.com/google/go-containerregistry/cmd/gcrane@latest +``` + +## Commands + +### ls + +`gcrane ls` exposes a more complex form of `ls` than `crane`, which allows for +listing tags, manifests, and sub-repositories. + +### cp + +`gcrane cp` supports a `-r` flag that copies images recursively, which is useful +for backing up images, georeplicating images, or renaming images en masse. + +### gc + +`gcrane gc` will calculate images that can be garbage-collected. +By default, it will print any images that do not have tags pointing to them. + +This can be composed with `gcrane delete` to actually garbage collect them: +```shell +gcrane gc gcr.io/${PROJECT_ID}/repo | xargs -n1 gcrane delete +``` + +## Images + +You can also use gcrane as docker image + +```sh +$ docker run --rm gcr.io/go-containerregistry/gcrane ls gcr.io/google-containers/busybox +gcr.io/google-containers/busybox@sha256:4bdd623e848417d96127e16037743f0cd8b528c026e9175e22a84f639eca58ff +gcr.io/google-containers/busybox:1.24 +gcr.io/google-containers/busybox@sha256:545e6a6310a27636260920bc07b994a299b6708a1b26910cfefd335fdfb60d2b +gcr.io/google-containers/busybox:1.27 +gcr.io/google-containers/busybox:1.27.2 +gcr.io/google-containers/busybox@sha256:d8d3bc2c183ed2f9f10e7258f84971202325ee6011ba137112e01e30f206de67 +gcr.io/google-containers/busybox:latest +``` + +And it's also available with a shell, at the `:debug` tag: + +```sh +docker run --rm -it --entrypoint "/busybox/sh" gcr.io/go-containerregistry/gcrane:debug +``` + +Tagged debug images are available at `gcr.io/go-containerregistry/gcrane/debug:[tag]`. diff --git a/cmd/gcrane/cmd/copy.go b/cmd/gcrane/cmd/copy.go new file mode 100644 index 0000000..5ec4224 --- /dev/null +++ b/cmd/gcrane/cmd/copy.go @@ -0,0 +1,47 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "runtime" + + "github.com/google/go-containerregistry/pkg/gcrane" + "github.com/spf13/cobra" +) + +// NewCmdCopy creates a new cobra.Command for the copy subcommand. +func NewCmdCopy() *cobra.Command { + recursive := false + jobs := 1 + cmd := &cobra.Command{ + Use: "copy SRC DST", + Aliases: []string{"cp"}, + Short: "Efficiently copy a remote image from src to dst", + Args: cobra.ExactArgs(2), + RunE: func(cc *cobra.Command, args []string) error { + src, dst := args[0], args[1] + ctx := cc.Context() + if recursive { + return gcrane.CopyRepository(ctx, src, dst, gcrane.WithJobs(jobs), gcrane.WithUserAgent(userAgent()), gcrane.WithContext(ctx)) + } + return gcrane.Copy(src, dst, gcrane.WithUserAgent(userAgent()), gcrane.WithContext(ctx)) + }, + } + + cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos") + cmd.Flags().IntVarP(&jobs, "jobs", "j", runtime.GOMAXPROCS(0), "The maximum number of concurrent copies") + + return cmd +} diff --git a/cmd/gcrane/cmd/gc.go b/cmd/gcrane/cmd/gc.go new file mode 100644 index 0000000..b377be4 --- /dev/null +++ b/cmd/gcrane/cmd/gc.go @@ -0,0 +1,76 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "fmt" + + "github.com/google/go-containerregistry/pkg/gcrane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/spf13/cobra" +) + +// NewCmdGc creates a new cobra.Command for the gc subcommand. +func NewCmdGc() *cobra.Command { + recursive := false + cmd := &cobra.Command{ + Use: "gc", + Short: "List images that are not tagged", + Args: cobra.ExactArgs(1), + RunE: func(cc *cobra.Command, args []string) error { + return gc(cc.Context(), args[0], recursive) + }, + } + + cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos") + + return cmd +} + +func gc(ctx context.Context, root string, recursive bool) error { + repo, err := name.NewRepository(root) + if err != nil { + return err + } + + opts := []google.Option{ + google.WithAuthFromKeychain(gcrane.Keychain), + google.WithUserAgent(userAgent()), + google.WithContext(ctx), + } + + if recursive { + return google.Walk(repo, printUntaggedImages, opts...) + } + + tags, err := google.List(repo, opts...) + return printUntaggedImages(repo, tags, err) +} + +func printUntaggedImages(repo name.Repository, tags *google.Tags, err error) error { + if err != nil { + return err + } + + for digest, manifest := range tags.Manifests { + if len(manifest.Tags) == 0 { + fmt.Printf("%s@%s\n", repo, digest) + } + } + + return nil +} diff --git a/cmd/gcrane/cmd/list.go b/cmd/gcrane/cmd/list.go new file mode 100644 index 0000000..892f2af --- /dev/null +++ b/cmd/gcrane/cmd/list.go @@ -0,0 +1,121 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "path" + + "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/google/go-containerregistry/pkg/gcrane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/spf13/cobra" +) + +func userAgent() string { + if cmd.Version != "" { + return path.Join("gcrane", cmd.Version) + } + + return "gcrane" +} + +// NewCmdList creates a new cobra.Command for the ls subcommand. +func NewCmdList() *cobra.Command { + recursive := false + json := false + cmd := &cobra.Command{ + Use: "ls REPO", + Short: "List the contents of a repo", + Args: cobra.ExactArgs(1), + RunE: func(cc *cobra.Command, args []string) error { + return ls(cc.Context(), args[0], recursive, json) + }, + } + + cmd.Flags().BoolVarP(&recursive, "recursive", "r", false, "Whether to recurse through repos") + cmd.Flags().BoolVar(&json, "json", false, "Format the response from the registry as JSON, one line per repo") + + return cmd +} + +func ls(ctx context.Context, root string, recursive, j bool) error { + repo, err := name.NewRepository(root) + if err != nil { + return err + } + + opts := []google.Option{ + google.WithAuthFromKeychain(gcrane.Keychain), + google.WithUserAgent(userAgent()), + google.WithContext(ctx), + } + + if recursive { + return google.Walk(repo, printImages(j), opts...) + } + + tags, err := google.List(repo, opts...) + if err != nil { + return err + } + + if !j { + if len(tags.Manifests) == 0 && len(tags.Children) == 0 { + // If we didn't see any GCR extensions, just list the tags like normal. + for _, tag := range tags.Tags { + fmt.Printf("%s:%s\n", repo, tag) + } + return nil + } + + // Since we're not recursing, print the subdirectories too. + for _, child := range tags.Children { + fmt.Printf("%s/%s\n", repo, child) + } + } + + return printImages(j)(repo, tags, err) +} + +func printImages(j bool) google.WalkFunc { + return func(repo name.Repository, tags *google.Tags, err error) error { + if err != nil { + return err + } + + if j { + b, err := json.Marshal(tags) + if err != nil { + return err + } + fmt.Printf("%s\n", b) + return nil + } + + for digest, manifest := range tags.Manifests { + fmt.Printf("%s@%s\n", repo, digest) + + for _, tag := range manifest.Tags { + fmt.Printf("%s:%s\n", repo, tag) + } + } + + return nil + } +} diff --git a/cmd/gcrane/depcheck_test.go b/cmd/gcrane/depcheck_test.go new file mode 100644 index 0000000..83749b3 --- /dev/null +++ b/cmd/gcrane/depcheck_test.go @@ -0,0 +1,32 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "testing" + + "github.com/google/go-containerregistry/internal/depcheck" +) + +func TestDeps(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow depcheck") + } + depcheck.AssertNoDependency(t, map[string][]string{ + "github.com/google/go-containerregistry/cmd/gcrane": { + "github.com/google/go-containerregistry/pkg/v1/daemon", + }, + }) +} diff --git a/cmd/gcrane/main.go b/cmd/gcrane/main.go new file mode 100644 index 0000000..98871e7 --- /dev/null +++ b/cmd/gcrane/main.go @@ -0,0 +1,72 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "os" + "os/signal" + + "github.com/google/go-containerregistry/cmd/crane/cmd" + gcmd "github.com/google/go-containerregistry/cmd/gcrane/cmd" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/gcrane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/spf13/cobra" +) + +func init() { + logs.Warn.SetOutput(os.Stderr) + logs.Progress.SetOutput(os.Stderr) +} + +const ( + use = "gcrane" + short = "gcrane is a tool for managing container images on gcr.io and pkg.dev" +) + +func main() { + options := []crane.Option{crane.WithAuthFromKeychain(gcrane.Keychain)} + // Same as crane, but override usage and keychain. + root := cmd.New(use, short, options) + + // Add or override commands. + gcraneCmds := []*cobra.Command{gcmd.NewCmdList(), gcmd.NewCmdGc(), gcmd.NewCmdCopy(), cmd.NewCmdAuth(options, "gcrane", "auth")} + + // Maintain a map of google-specific commands that we "override". + used := make(map[string]bool) + for _, cmd := range gcraneCmds { + used[cmd.Use] = true + } + + // Remove those from crane's set of commands. + for _, cmd := range root.Commands() { + if _, ok := used[cmd.Use]; ok { + root.RemoveCommand(cmd) + } + } + + // Add our own. + for _, cmd := range gcraneCmds { + root.AddCommand(cmd) + } + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + if err := root.ExecuteContext(ctx); err != nil { + cancel() + os.Exit(1) + } +} diff --git a/cmd/ko/README.md b/cmd/ko/README.md new file mode 100644 index 0000000..7f3627e --- /dev/null +++ b/cmd/ko/README.md @@ -0,0 +1,3 @@ +# `ko` has moved + +Please find `ko` at its new home, https://github.com/google/ko diff --git a/cmd/krane/README.md b/cmd/krane/README.md new file mode 100644 index 0000000..2f21e00 --- /dev/null +++ b/cmd/krane/README.md @@ -0,0 +1,15 @@ +# `krane` + +<img src="../../images/crane.png" width="40%"> + +This tool is a variant of the [`crane`](../crane/README.md) command, but builds in +support for authenticating against registries using common credential helpers +that find credentials from the environment. + +In particular this tool supports authenticating with common "workload identity" +mechanisms on platforms such as GKE and EKS. + +This additional keychain logic only kicks in if alternative authentication +mechanisms have NOT been configured and `crane` would otherwise perform the +command without credentials, so **it is a drop-in replacement for `crane` that +adds support for authenticating with cloud workload identity mechanisms**. diff --git a/cmd/krane/go.mod b/cmd/krane/go.mod new file mode 100644 index 0000000..3ca44a8 --- /dev/null +++ b/cmd/krane/go.mod @@ -0,0 +1,67 @@ +module github.com/google/go-containerregistry/cmd/krane + +go 1.18 + +replace github.com/google/go-containerregistry => ../../ + +require ( + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 + github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 + github.com/google/go-containerregistry v0.13.0 +) + +require ( + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.28 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect + github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect + github.com/aws/smithy-go v1.13.5 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/cli v23.0.1+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v23.0.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/cobra v1.6.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gotest.tools/v3 v3.1.0 // indirect +) diff --git a/cmd/krane/go.sum b/cmd/krane/go.sum new file mode 100644 index 0000000..23db7e4 --- /dev/null +++ b/cmd/krane/go.sum @@ -0,0 +1,199 @@ +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= +github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY= +github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 h1:tGA4ZoAsrYhGBypKAo2jwoX/Z5ponBZOTEUMNN/rHP4= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5/go.mod h1:cDZh+PHP8Adt9E0zfZT9cK4qadbtIuU/czLpEJtm4wc= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 h1:6OBVD6KE4gLReaNfG7CSXFvNIVqKIqrywRcG1kUKr4M= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4/go.mod h1:gUxgbzXs+gHsj/6al9dzzoByeSrEl03Oj4iJBu/m/Rk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 h1:uQhxQriOPUu/knXSPM7D/VyS3GMz+4wsE43eB8f9ojg= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1/go.mod h1:/JmJjW2NJpzRSI3pOxQPC6eOD/tR8SfOA9X1FurmzXI= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= diff --git a/cmd/krane/main.go b/cmd/krane/main.go new file mode 100644 index 0000000..6912463 --- /dev/null +++ b/cmd/krane/main.go @@ -0,0 +1,67 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "io" + "os" + "os/signal" + + ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" + "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" + "github.com/google/go-containerregistry/cmd/crane/cmd" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/v1/google" +) + +var ( + amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))) + azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) +) + +func init() { + logs.Warn.SetOutput(os.Stderr) + logs.Progress.SetOutput(os.Stderr) +} + +const ( + use = "krane" + short = "krane is a tool for managing container images" +) + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + keychain := authn.NewMultiKeychain( + authn.DefaultKeychain, + google.Keychain, + github.Keychain, + amazonKeychain, + azureKeychain, + ) + + // Same as crane, but override usage and keychain. + root := cmd.New(use, short, []crane.Option{crane.WithAuthFromKeychain(keychain)}) + + if err := root.ExecuteContext(ctx); err != nil { + cancel() + os.Exit(1) + } +} diff --git a/cmd/registry/main.go b/cmd/registry/main.go new file mode 100644 index 0000000..58a0e4e --- /dev/null +++ b/cmd/registry/main.go @@ -0,0 +1,44 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "flag" + "fmt" + "log" + "net" + "net/http" + "time" + + "github.com/google/go-containerregistry/pkg/registry" +) + +var port = flag.Int("port", 1338, "port to run registry on") + +func main() { + flag.Parse() + + listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port)) + if err != nil { + log.Fatal(err) + } + porti := listener.Addr().(*net.TCPAddr).Port + log.Printf("serving on port %d", porti) + s := &http.Server{ + ReadHeaderTimeout: 5 * time.Second, // prevent slowloris, quiet linter + Handler: registry.New(), + } + log.Fatal(s.Serve(listener)) +} diff --git a/cmd/registry/test.sh b/cmd/registry/test.sh new file mode 100755 index 0000000..d0c9507 --- /dev/null +++ b/cmd/registry/test.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -ex + +CONTAINER_OS=$(docker info -f '{{ .OSType }}') + +# crane can run on a Windows system, but doesn't currently support pulling Windows +# containers, so this test can only run if Docker is in Linux container mode. +if [[ ${CONTAINER_OS} = "windows" ]]; then + set +x + echo [TEST SKIPPED] Windows containers are not yet supported by crane + exit +fi + +function cleanup { + [[ -n $PID ]] && kill $PID + [[ -n $CTR ]] && docker stop $CTR + rm -f ubuntu.tar debiand.tar debianc.tar + docker rmi -f \ + localhost:1338/debianc:latest \ + localhost:1338/debiand:latest \ + localhost:1338/ubuntuc:foo \ + localhost:1338/ubuntud:latest \ + || true +} +trap cleanup EXIT + +case "$OSTYPE" in + # On Windows, Docker runs in a VM, so a registry running on the Windows + # host is not accessible via localhost for `docker pull|push`. + win*|msys*|cygwin*) + docker run -d --rm -p 1338:5000 --name test-reg registry:2 + CTR=test-reg + ;; + + *) + registry & + PID=$! + ;; +esac + +go install ./cmd/registry +go install ./cmd/crane + + +crane pull debian:latest debianc.tar +crane push debianc.tar localhost:1338/debianc:latest +docker pull localhost:1338/debianc:latest +docker tag localhost:1338/debianc:latest localhost:1338/debiand:latest +docker push localhost:1338/debiand:latest +crane pull localhost:1338/debiand:latest debiand.tar + +docker pull ubuntu:latest +docker tag ubuntu:latest localhost:1338/ubuntud:latest +docker push localhost:1338/ubuntud:latest +crane pull localhost:1338/ubuntud:latest ubuntu.tar +crane push ubuntu.tar localhost:1338/ubuntuc:foo +docker pull localhost:1338/ubuntuc:foo @@ -0,0 +1,49 @@ +module github.com/google/go-containerregistry + +go 1.18 + +require ( + github.com/containerd/stargz-snapshotter/estargz v0.14.3 + github.com/docker/cli v23.0.1+incompatible + github.com/docker/distribution v2.8.1+incompatible + github.com/docker/docker v23.0.1+incompatible + github.com/google/go-cmp v0.5.9 + github.com/klauspost/compress v1.16.0 + github.com/mitchellh/go-homedir v1.1.0 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.0-rc2 + github.com/spf13/cobra v1.6.1 + golang.org/x/oauth2 v0.6.0 + golang.org/x/sync v0.1.0 + golang.org/x/tools v0.7.0 +) + +require ( + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/vbatts/tar-split v0.11.2 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.0.3 // indirect +) @@ -0,0 +1,153 @@ +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= +github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/hack/boilerplate/boilerplate.go.txt b/hack/boilerplate/boilerplate.go.txt new file mode 100644 index 0000000..a237f5e --- /dev/null +++ b/hack/boilerplate/boilerplate.go.txt @@ -0,0 +1,13 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. diff --git a/hack/bump-deps.sh b/hack/bump-deps.sh new file mode 100755 index 0000000..0e7325d --- /dev/null +++ b/hack/bump-deps.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +pushd ${PROJECT_ROOT} +trap popd EXIT + +go get -u ./... +go mod tidy -compat=1.18 +go mod vendor + +cd ${PROJECT_ROOT}/pkg/authn/k8schain +go get -u ./... +go mod tidy -compat=1.18 +go mod download + +cd ${PROJECT_ROOT}/pkg/authn/kubernetes +go get -u ./... +go mod tidy -compat=1.18 +go mod download + +cd ${PROJECT_ROOT}/cmd/krane +go get -u ./... +go mod tidy -compat=1.18 +go mod download + +cd ${PROJECT_ROOT} + +./hack/update-deps.sh diff --git a/hack/presubmit.sh b/hack/presubmit.sh new file mode 100755 index 0000000..25afe44 --- /dev/null +++ b/hack/presubmit.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# We can't install in the current directory without changing the current module. +TMP_DIR="$(mktemp -d)" +export PATH="${PATH}:${TMP_DIR}/bin" +export GOPATH="${TMP_DIR}" +pushd ${TMP_DIR} +trap popd EXIT +go install honnef.co/go/tools/cmd/staticcheck@v0.3.3 +popd + +pushd ${PROJECT_ROOT} +trap popd EXIT + +staticcheck ./pkg/... + +# Verify that all source files are correctly formatted. +find . -name "*.go" | grep -v vendor/ | xargs gofmt -d -e -l + +# Verify that generated crane docs are up-to-date. +mkdir -p /tmp/gendoc && go run cmd/crane/help/main.go --dir /tmp/gendoc && diff -Naur /tmp/gendoc/ cmd/crane/doc/ + +go test ./... +./pkg/name/internal/must_test.sh + +./cmd/crane/rebase_test.sh + +pushd ${PROJECT_ROOT}/cmd/krane +trap popd EXIT +go build ./... + +pushd ${PROJECT_ROOT}/pkg/authn/k8schain +trap popd EXIT +go build ./... + +pushd ${PROJECT_ROOT}/pkg/authn/kubernetes +trap popd EXIT +go test ./... diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh new file mode 100755 index 0000000..e237a50 --- /dev/null +++ b/hack/update-codegen.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BOILER_PLATE_FILE="${PROJECT_ROOT}/hack/boilerplate/boilerplate.go.txt" +MODULE_NAME=github.com/google/go-containerregistry + +pushd ${PROJECT_ROOT} +trap popd EXIT + +export GOPATH=$(go env GOPATH) +export PATH="${GOPATH}/bin:${PATH}" + +go mod tidy +go mod vendor + +export GOBIN=$(mktemp -d) +export PATH="$GOBIN:$PATH" + +go install github.com/maxbrunsfeld/counterfeiter/v6@latest +go install k8s.io/code-generator/cmd/deepcopy-gen@v0.20.7 + +counterfeiter -o pkg/v1/fake/index.go ${PROJECT_ROOT}/pkg/v1 ImageIndex +counterfeiter -o pkg/v1/fake/image.go ${PROJECT_ROOT}/pkg/v1 Image + +DEEPCOPY_OUTPUT=$(mktemp -d) + +deepcopy-gen -O zz_deepcopy_generated --go-header-file $BOILER_PLATE_FILE \ + --input-dirs "$MODULE_NAME/pkg/v1" \ + --output-base "$DEEPCOPY_OUTPUT" + +# TODO - Generalize this for all directories when we need it +cp $DEEPCOPY_OUTPUT/$MODULE_NAME/pkg/v1/*.go $PROJECT_ROOT/pkg/v1 + +go run $PROJECT_ROOT/cmd/crane/help/main.go --dir=$PROJECT_ROOT/cmd/crane/doc/ diff --git a/hack/update-deps.sh b/hack/update-deps.sh new file mode 100755 index 0000000..25be810 --- /dev/null +++ b/hack/update-deps.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Copyright 2018 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +pushd ${PROJECT_ROOT} +trap popd EXIT + +go mod tidy +go mod vendor + +# Delete all vendored broken symlinks. +# From https://stackoverflow.com/questions/22097130/delete-all-broken-symbolic-links-with-a-line +find vendor/ -type l -exec sh -c 'for x; do [ -e "$x" ] || rm "$x"; done' _ {} + diff --git a/hack/update-dots.sh b/hack/update-dots.sh new file mode 100755 index 0000000..570c794 --- /dev/null +++ b/hack/update-dots.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Copyright 2019 The original author or authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +pushd ${PROJECT_ROOT} +trap popd EXIT + +dot -Tjpeg images/ociimage.gv > images/ociimage.jpeg + +for f in $(ls images/dot/ | grep -e '.dot$'); do + dot -Tsvg images/dot/$f > images/$f.svg +done diff --git a/images/containerd.dot.svg b/images/containerd.dot.svg new file mode 100644 index 0000000..cb87da6 --- /dev/null +++ b/images/containerd.dot.svg @@ -0,0 +1,2074 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: godep Pages: 1 --> +<svg width="7819pt" height="984pt" + viewBox="0.00 0.00 7819.00 984.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 980)"> +<title>godep</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-980 7815,-980 7815,4 -4,4"/> +<!-- bufio --> +<g id="node1" class="node"> +<title>bufio</title> +<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4541,-36C4541,-36 4511,-36 4511,-36 4505,-36 4499,-30 4499,-24 4499,-24 4499,-12 4499,-12 4499,-6 4505,0 4511,0 4511,0 4541,0 4541,0 4547,0 4553,-6 4553,-12 4553,-12 4553,-24 4553,-24 4553,-30 4547,-36 4541,-36"/> +<text text-anchor="middle" x="4526" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text> +</a> +</g> +</g> +<!-- bytes --> +<g id="node2" class="node"> +<title>bytes</title> +<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4991,-36C4991,-36 4961,-36 4961,-36 4955,-36 4949,-30 4949,-24 4949,-24 4949,-12 4949,-12 4949,-6 4955,0 4961,0 4961,0 4991,0 4991,0 4997,0 5003,-6 5003,-12 5003,-12 5003,-24 5003,-24 5003,-30 4997,-36 4991,-36"/> +<text text-anchor="middle" x="4976" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text> +</a> +</g> +</g> +<!-- compress/gzip --> +<g id="node3" class="node"> +<title>compress/gzip</title> +<g id="a_node3"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1425.5,-318C1425.5,-318 1354.5,-318 1354.5,-318 1348.5,-318 1342.5,-312 1342.5,-306 1342.5,-306 1342.5,-294 1342.5,-294 1342.5,-288 1348.5,-282 1354.5,-282 1354.5,-282 1425.5,-282 1425.5,-282 1431.5,-282 1437.5,-288 1437.5,-294 1437.5,-294 1437.5,-306 1437.5,-306 1437.5,-312 1431.5,-318 1425.5,-318"/> +<text text-anchor="middle" x="1390" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text> +</a> +</g> +</g> +<!-- container/list --> +<g id="node4" class="node"> +<title>container/list</title> +<g id="a_node4"><a xlink:href="https://godoc.org/container/list" xlink:title="container/list" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4535.5,-130C4535.5,-130 4472.5,-130 4472.5,-130 4466.5,-130 4460.5,-124 4460.5,-118 4460.5,-118 4460.5,-106 4460.5,-106 4460.5,-100 4466.5,-94 4472.5,-94 4472.5,-94 4535.5,-94 4535.5,-94 4541.5,-94 4547.5,-100 4547.5,-106 4547.5,-106 4547.5,-118 4547.5,-118 4547.5,-124 4541.5,-130 4535.5,-130"/> +<text text-anchor="middle" x="4504" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">container/list</text> +</a> +</g> +</g> +<!-- context --> +<g id="node5" class="node"> +<title>context</title> +<g id="a_node5"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2490,-130C2490,-130 2458,-130 2458,-130 2452,-130 2446,-124 2446,-118 2446,-118 2446,-106 2446,-106 2446,-100 2452,-94 2458,-94 2458,-94 2490,-94 2490,-94 2496,-94 2502,-100 2502,-106 2502,-106 2502,-118 2502,-118 2502,-124 2496,-130 2490,-130"/> +<text text-anchor="middle" x="2474" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text> +</a> +</g> +</g> +<!-- crypto --> +<g id="node6" class="node"> +<title>crypto</title> +<g id="a_node6"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2064,-224C2064,-224 2034,-224 2034,-224 2028,-224 2022,-218 2022,-212 2022,-212 2022,-200 2022,-200 2022,-194 2028,-188 2034,-188 2034,-188 2064,-188 2064,-188 2070,-188 2076,-194 2076,-200 2076,-200 2076,-212 2076,-212 2076,-218 2070,-224 2064,-224"/> +<text text-anchor="middle" x="2049" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text> +</a> +</g> +</g> +<!-- encoding --> +<g id="node7" class="node"> +<title>encoding</title> +<g id="a_node7"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4030,-36C4030,-36 3988,-36 3988,-36 3982,-36 3976,-30 3976,-24 3976,-24 3976,-12 3976,-12 3976,-6 3982,0 3988,0 3988,0 4030,0 4030,0 4036,0 4042,-6 4042,-12 4042,-12 4042,-24 4042,-24 4042,-30 4036,-36 4030,-36"/> +<text text-anchor="middle" x="4009" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text> +</a> +</g> +</g> +<!-- encoding/base64 --> +<g id="node8" class="node"> +<title>encoding/base64</title> +<g id="a_node8"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5682.5,-788C5682.5,-788 5599.5,-788 5599.5,-788 5593.5,-788 5587.5,-782 5587.5,-776 5587.5,-776 5587.5,-764 5587.5,-764 5587.5,-758 5593.5,-752 5599.5,-752 5599.5,-752 5682.5,-752 5682.5,-752 5688.5,-752 5694.5,-758 5694.5,-764 5694.5,-764 5694.5,-776 5694.5,-776 5694.5,-782 5688.5,-788 5682.5,-788"/> +<text text-anchor="middle" x="5641" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text> +</a> +</g> +</g> +<!-- encoding/json --> +<g id="node9" class="node"> +<title>encoding/json</title> +<g id="a_node9"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5859,-36C5859,-36 5791,-36 5791,-36 5785,-36 5779,-30 5779,-24 5779,-24 5779,-12 5779,-12 5779,-6 5785,0 5791,0 5791,0 5859,0 5859,0 5865,0 5871,-6 5871,-12 5871,-12 5871,-24 5871,-24 5871,-30 5865,-36 5859,-36"/> +<text text-anchor="middle" x="5825" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text> +</a> +</g> +</g> +<!-- errors --> +<g id="node10" class="node"> +<title>errors</title> +<g id="a_node10"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4332,-36C4332,-36 4302,-36 4302,-36 4296,-36 4290,-30 4290,-24 4290,-24 4290,-12 4290,-12 4290,-6 4296,0 4302,0 4302,0 4332,0 4332,0 4338,0 4344,-6 4344,-12 4344,-12 4344,-24 4344,-24 4344,-30 4338,-36 4332,-36"/> +<text text-anchor="middle" x="4317" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text> +</a> +</g> +</g> +<!-- fmt --> +<g id="node11" class="node"> +<title>fmt</title> +<g id="a_node11"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2902,-36C2902,-36 2872,-36 2872,-36 2866,-36 2860,-30 2860,-24 2860,-24 2860,-12 2860,-12 2860,-6 2866,0 2872,0 2872,0 2902,0 2902,0 2908,0 2914,-6 2914,-12 2914,-12 2914,-24 2914,-24 2914,-30 2908,-36 2902,-36"/> +<text text-anchor="middle" x="2887" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression --> +<g id="node12" class="node"> +<title>github.com/containerd/containerd/archive/compression</title> +<g id="a_node12"><a xlink:href="https://godoc.org/github.com/containerd/containerd/archive/compression" xlink:title="github.com/containerd/containerd/archive/compression" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2617.5,-412C2617.5,-412 2324.5,-412 2324.5,-412 2318.5,-412 2312.5,-406 2312.5,-400 2312.5,-400 2312.5,-388 2312.5,-388 2312.5,-382 2318.5,-376 2324.5,-376 2324.5,-376 2617.5,-376 2617.5,-376 2623.5,-376 2629.5,-382 2629.5,-388 2629.5,-388 2629.5,-400 2629.5,-400 2629.5,-406 2623.5,-412 2617.5,-412"/> +<text text-anchor="middle" x="2471" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/archive/compression</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->bufio --> +<g id="edge1" class="edge"> +<title>github.com/containerd/containerd/archive/compression->bufio</title> +<path fill="none" stroke="#000000" d="M2629.602,-388.754C3019.2578,-375.5526 3996.8201,-340.4127 4064,-318 4232.1526,-261.9007 4243.7536,-188.0662 4394,-94 4427.6882,-72.9085 4467.3208,-50.3852 4494.3045,-35.382"/> +<polygon fill="#000000" stroke="#000000" points="4495.3344,-36.8119 4498.857,-32.8554 4493.636,-33.7516 4495.3344,-36.8119"/> +</g> +<!-- github.com/containerd/containerd/archive/compression->bytes --> +<g id="edge2" class="edge"> +<title>github.com/containerd/containerd/archive/compression->bytes</title> +<path fill="none" stroke="#000000" d="M2629.7757,-388.9701C3030.935,-375.9594 4058.4564,-340.6347 4129,-318 4202.1293,-294.5356 4211.1021,-268.0718 4274,-224 4293.619,-210.2533 4423.8873,-103.2143 4446,-94 4469.2273,-84.3212 4832.4387,-36.6303 4943.7948,-22.1661"/> +<polygon fill="#000000" stroke="#000000" points="4944.1717,-23.882 4948.9048,-21.5028 4943.721,-20.4111 4944.1717,-23.882"/> +</g> +<!-- github.com/containerd/containerd/archive/compression->compress/gzip --> +<g id="edge3" class="edge"> +<title>github.com/containerd/containerd/archive/compression->compress/gzip</title> +<path fill="none" stroke="#000000" d="M2312.1441,-386.1973C2068.1437,-373.5259 1612.5067,-346.9587 1452,-318 1448.9598,-317.4515 1445.8555,-316.8124 1442.7363,-316.1087"/> +<polygon fill="#000000" stroke="#000000" points="1443.0972,-314.3959 1437.8292,-314.9533 1442.295,-317.8027 1443.0972,-314.3959"/> +</g> +<!-- github.com/containerd/containerd/archive/compression->context --> +<g id="edge4" class="edge"> +<title>github.com/containerd/containerd/archive/compression->context</title> +<path fill="none" stroke="#000000" d="M2454.5412,-375.8636C2423.8251,-339.7074 2363.4457,-256.2566 2393,-188 2402.8888,-165.1614 2423.3943,-146.0342 2441.2983,-132.6811"/> +<polygon fill="#000000" stroke="#000000" points="2442.7751,-133.7705 2445.786,-129.4119 2440.7142,-130.9415 2442.7751,-133.7705"/> +</g> +<!-- github.com/containerd/containerd/archive/compression->fmt --> +<g id="edge5" class="edge"> +<title>github.com/containerd/containerd/archive/compression->fmt</title> +<path fill="none" stroke="#000000" d="M2312.4669,-390.5832C2122.5633,-384.3347 1817.224,-366.8098 1715,-318 1618.7665,-272.0505 1553.068,-168.877 1629,-94 1673.9782,-49.6467 2661.7572,-23.4634 2854.544,-18.766"/> +<polygon fill="#000000" stroke="#000000" points="2854.7933,-20.5105 2859.7494,-18.6396 2854.7084,-17.0115 2854.7933,-20.5105"/> +</g> +<!-- github.com/containerd/containerd/log --> +<g id="node13" class="node"> +<title>github.com/containerd/containerd/log</title> +<g id="a_node13"><a xlink:href="https://godoc.org/github.com/containerd/containerd/log" xlink:title="github.com/containerd/containerd/log" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5049.5,-318C5049.5,-318 4852.5,-318 4852.5,-318 4846.5,-318 4840.5,-312 4840.5,-306 4840.5,-306 4840.5,-294 4840.5,-294 4840.5,-288 4846.5,-282 4852.5,-282 4852.5,-282 5049.5,-282 5049.5,-282 5055.5,-282 5061.5,-288 5061.5,-294 5061.5,-294 5061.5,-306 5061.5,-306 5061.5,-312 5055.5,-318 5049.5,-318"/> +<text text-anchor="middle" x="4951" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/log</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->github.com/containerd/containerd/log --> +<g id="edge6" class="edge"> +<title>github.com/containerd/containerd/archive/compression->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M2629.6722,-388.3943C2974.2953,-376.1595 3817.0248,-345.9187 4523,-318 4628.6495,-313.822 4748.9534,-308.7331 4835.0523,-305.0336"/> +<polygon fill="#000000" stroke="#000000" points="4835.2364,-306.7774 4840.1566,-304.8142 4835.086,-303.2806 4835.2364,-306.7774"/> +</g> +<!-- io --> +<g id="node14" class="node"> +<title>io</title> +<g id="a_node14"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1035,-36C1035,-36 1005,-36 1005,-36 999,-36 993,-30 993,-24 993,-24 993,-12 993,-12 993,-6 999,0 1005,0 1005,0 1035,0 1035,0 1041,0 1047,-6 1047,-12 1047,-12 1047,-24 1047,-24 1047,-30 1041,-36 1035,-36"/> +<text text-anchor="middle" x="1020" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->io --> +<g id="edge7" class="edge"> +<title>github.com/containerd/containerd/archive/compression->io</title> +<path fill="none" stroke="#000000" d="M2312.4732,-389.5287C2019.6144,-380.3798 1415.5593,-357.0299 1328,-318 1187.4632,-255.3551 1073.5571,-99.0913 1034.5113,-40.5866"/> +<polygon fill="#000000" stroke="#000000" points="1035.835,-39.4161 1031.6129,-36.2165 1032.9182,-41.3507 1035.835,-39.4161"/> +</g> +<!-- os --> +<g id="node15" class="node"> +<title>os</title> +<g id="a_node15"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3787,-36C3787,-36 3757,-36 3757,-36 3751,-36 3745,-30 3745,-24 3745,-24 3745,-12 3745,-12 3745,-6 3751,0 3757,0 3757,0 3787,0 3787,0 3793,0 3799,-6 3799,-12 3799,-12 3799,-24 3799,-24 3799,-30 3793,-36 3787,-36"/> +<text text-anchor="middle" x="3772" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->os --> +<g id="edge8" class="edge"> +<title>github.com/containerd/containerd/archive/compression->os</title> +<path fill="none" stroke="#000000" d="M2629.7853,-389.1047C3021.1366,-376.6656 4002.1395,-343.0552 4026,-318 4244.507,-88.5527 3799.4971,-190.9797 3750,-130 3729.3083,-104.5081 3744.6162,-65.0981 3757.9921,-40.4644"/> +<polygon fill="#000000" stroke="#000000" points="3759.5508,-41.2626 3760.4581,-36.0434 3756.4941,-39.5576 3759.5508,-41.2626"/> +</g> +<!-- os/exec --> +<g id="node16" class="node"> +<title>os/exec</title> +<g id="a_node16"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1511.5,-318C1511.5,-318 1478.5,-318 1478.5,-318 1472.5,-318 1466.5,-312 1466.5,-306 1466.5,-306 1466.5,-294 1466.5,-294 1466.5,-288 1472.5,-282 1478.5,-282 1478.5,-282 1511.5,-282 1511.5,-282 1517.5,-282 1523.5,-288 1523.5,-294 1523.5,-294 1523.5,-306 1523.5,-306 1523.5,-312 1517.5,-318 1511.5,-318"/> +<text text-anchor="middle" x="1495" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->os/exec --> +<g id="edge9" class="edge"> +<title>github.com/containerd/containerd/archive/compression->os/exec</title> +<path fill="none" stroke="#000000" d="M2312.3273,-385.8993C2087.5074,-373.5761 1687.8131,-348.3206 1546,-318 1540.2613,-316.773 1534.2849,-315.0723 1528.5288,-313.2022"/> +<polygon fill="#000000" stroke="#000000" points="1529.0527,-311.5321 1523.7556,-311.5982 1527.9377,-314.8498 1529.0527,-311.5321"/> +</g> +<!-- strconv --> +<g id="node17" class="node"> +<title>strconv</title> +<g id="a_node17"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M802,-36C802,-36 770,-36 770,-36 764,-36 758,-30 758,-24 758,-24 758,-12 758,-12 758,-6 764,0 770,0 770,0 802,0 802,0 808,0 814,-6 814,-12 814,-12 814,-24 814,-24 814,-30 808,-36 802,-36"/> +<text text-anchor="middle" x="786" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->strconv --> +<g id="edge10" class="edge"> +<title>github.com/containerd/containerd/archive/compression->strconv</title> +<path fill="none" stroke="#000000" d="M2312.3702,-388.4396C2006.6929,-377.082 1357.5028,-349.6065 1260,-318 1067.887,-255.7247 875.992,-97.4925 810.0182,-39.6228"/> +<polygon fill="#000000" stroke="#000000" points="811.136,-38.2753 806.2265,-36.2855 808.8236,-40.9027 811.136,-38.2753"/> +</g> +<!-- sync --> +<g id="node18" class="node"> +<title>sync</title> +<g id="a_node18"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5596,-36C5596,-36 5566,-36 5566,-36 5560,-36 5554,-30 5554,-24 5554,-24 5554,-12 5554,-12 5554,-6 5560,0 5566,0 5566,0 5596,0 5596,0 5602,0 5608,-6 5608,-12 5608,-12 5608,-24 5608,-24 5608,-30 5602,-36 5596,-36"/> +<text text-anchor="middle" x="5581" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/archive/compression->sync --> +<g id="edge11" class="edge"> +<title>github.com/containerd/containerd/archive/compression->sync</title> +<path fill="none" stroke="#000000" d="M2629.6865,-390.0124C2932.6896,-381.724 3614.4742,-359.8289 4187,-318 4392.4035,-302.9932 4908.1504,-269.5549 5109,-224 5229.6478,-196.6357 5257.8998,-180.1302 5371,-130 5435.9761,-101.2002 5508.4426,-60.5387 5548.9999,-36.9573"/> +<polygon fill="#000000" stroke="#000000" points="5550.3466,-38.1979 5553.785,-34.1681 5548.584,-35.1741 5550.3466,-38.1979"/> +</g> +<!-- github.com/containerd/containerd/log->context --> +<g id="edge45" class="edge"> +<title>github.com/containerd/containerd/log->context</title> +<path fill="none" stroke="#000000" d="M4840.3081,-297.0826C4501.692,-287.8796 3482.8155,-258.045 3153,-224 2903.1457,-198.2089 2605.7286,-139.242 2507.3818,-118.9847"/> +<polygon fill="#000000" stroke="#000000" points="2507.5459,-117.2317 2502.2954,-117.9349 2506.8384,-120.6595 2507.5459,-117.2317"/> +</g> +<!-- github.com/sirupsen/logrus --> +<g id="node36" class="node"> +<title>github.com/sirupsen/logrus</title> +<g id="a_node36"><a xlink:href="https://godoc.org/github.com/sirupsen/logrus" xlink:title="github.com/sirupsen/logrus" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M5082.5,-224C5082.5,-224 4941.5,-224 4941.5,-224 4935.5,-224 4929.5,-218 4929.5,-212 4929.5,-212 4929.5,-200 4929.5,-200 4929.5,-194 4935.5,-188 4941.5,-188 4941.5,-188 5082.5,-188 5082.5,-188 5088.5,-188 5094.5,-194 5094.5,-200 5094.5,-200 5094.5,-212 5094.5,-212 5094.5,-218 5088.5,-224 5082.5,-224"/> +<text text-anchor="middle" x="5012" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/sirupsen/logrus</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/log->github.com/sirupsen/logrus --> +<g id="edge46" class="edge"> +<title>github.com/containerd/containerd/log->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M4962.7614,-281.8759C4972.6349,-266.661 4986.7857,-244.8548 4997.3856,-228.5205"/> +<polygon fill="#000000" stroke="#000000" points="4999.0208,-229.2154 5000.2747,-224.0685 4996.0849,-227.3101 4999.0208,-229.2154"/> +</g> +<!-- sync/atomic --> +<g id="node37" class="node"> +<title>sync/atomic</title> +<g id="a_node37"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4798.5,-36C4798.5,-36 4739.5,-36 4739.5,-36 4733.5,-36 4727.5,-30 4727.5,-24 4727.5,-24 4727.5,-12 4727.5,-12 4727.5,-6 4733.5,0 4739.5,0 4739.5,0 4798.5,0 4798.5,0 4804.5,0 4810.5,-6 4810.5,-12 4810.5,-12 4810.5,-24 4810.5,-24 4810.5,-30 4804.5,-36 4798.5,-36"/> +<text text-anchor="middle" x="4769" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/log->sync/atomic --> +<g id="edge47" class="edge"> +<title>github.com/containerd/containerd/log->sync/atomic</title> +<path fill="none" stroke="#000000" d="M5018.697,-281.9819C5073.108,-263.8348 5136.3106,-232.1895 5109,-188 5075.6291,-134.0048 4900.4349,-65.1685 4815.397,-34.2821"/> +<polygon fill="#000000" stroke="#000000" points="4815.9694,-32.6282 4810.6722,-32.5714 4814.7778,-35.9192 4815.9694,-32.6282"/> +</g> +<!-- github.com/containerd/containerd/content --> +<g id="node19" class="node"> +<title>github.com/containerd/containerd/content</title> +<g id="a_node19"><a xlink:href="https://godoc.org/github.com/containerd/containerd/content" xlink:title="github.com/containerd/containerd/content" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2107,-600C2107,-600 1887,-600 1887,-600 1881,-600 1875,-594 1875,-588 1875,-588 1875,-576 1875,-576 1875,-570 1881,-564 1887,-564 1887,-564 2107,-564 2107,-564 2113,-564 2119,-570 2119,-576 2119,-576 2119,-588 2119,-588 2119,-594 2113,-600 2107,-600"/> +<text text-anchor="middle" x="1997" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/content</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->context --> +<g id="edge12" class="edge"> +<title>github.com/containerd/containerd/content->context</title> +<path fill="none" stroke="#000000" d="M2009.4378,-563.7508C2061.0674,-488.0798 2256.6557,-202.2539 2275,-188 2325.5117,-148.7511 2398.6126,-127.8448 2440.7181,-118.4121"/> +<polygon fill="#000000" stroke="#000000" points="2441.1878,-120.1007 2445.6956,-117.3182 2440.4365,-116.6823 2441.1878,-120.1007"/> +</g> +<!-- github.com/containerd/containerd/content->io --> +<g id="edge17" class="edge"> +<title>github.com/containerd/containerd/content->io</title> +<path fill="none" stroke="#000000" d="M1944.6218,-563.9032C1776.8372,-505.8897 1262.6845,-327.7606 1247,-318 1149.3067,-257.2043 1130.5949,-227.1914 1069,-130 1050.7564,-101.2132 1036.1205,-64.4732 1027.7444,-41.0836"/> +<polygon fill="#000000" stroke="#000000" points="1029.3705,-40.4329 1026.053,-36.3029 1026.0709,-41.6003 1029.3705,-40.4329"/> +</g> +<!-- github.com/containerd/containerd/content->sync --> +<g id="edge20" class="edge"> +<title>github.com/containerd/containerd/content->sync</title> +<path fill="none" stroke="#000000" d="M2119.1524,-569.2682C2686.4218,-510.022 5042.6342,-262.568 5195,-224 5303.6508,-196.4975 5331.1031,-184.5672 5429,-130 5476.7566,-103.3807 5526.8082,-63.9031 5555.8907,-39.6231"/> +<polygon fill="#000000" stroke="#000000" points="5557.3202,-40.7083 5560.0267,-36.1545 5555.0711,-38.0266 5557.3202,-40.7083"/> +</g> +<!-- github.com/containerd/containerd/errdefs --> +<g id="node20" class="node"> +<title>github.com/containerd/containerd/errdefs</title> +<g id="a_node20"><a xlink:href="https://godoc.org/github.com/containerd/containerd/errdefs" xlink:title="github.com/containerd/containerd/errdefs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3363,-506C3363,-506 3145,-506 3145,-506 3139,-506 3133,-500 3133,-494 3133,-494 3133,-482 3133,-482 3133,-476 3139,-470 3145,-470 3145,-470 3363,-470 3363,-470 3369,-470 3375,-476 3375,-482 3375,-482 3375,-494 3375,-494 3375,-500 3369,-506 3363,-506"/> +<text text-anchor="middle" x="3254" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/errdefs</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->github.com/containerd/containerd/errdefs --> +<g id="edge13" class="edge"> +<title>github.com/containerd/containerd/content->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M2119.1708,-575.9524C2324.3756,-565.2821 2751.1532,-540.8938 3111,-506 3116.4345,-505.473 3121.9814,-504.9051 3127.5822,-504.3071"/> +<polygon fill="#000000" stroke="#000000" points="3127.9562,-506.0269 3132.7392,-503.7497 3127.5801,-502.5472 3127.9562,-506.0269"/> +</g> +<!-- github.com/opencontainers/go-digest --> +<g id="node21" class="node"> +<title>github.com/opencontainers/go-digest</title> +<g id="a_node21"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go-digest" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M2153,-318C2153,-318 1959,-318 1959,-318 1953,-318 1947,-312 1947,-306 1947,-306 1947,-294 1947,-294 1947,-288 1953,-282 1959,-282 1959,-282 2153,-282 2153,-282 2159,-282 2165,-288 2165,-294 2165,-294 2165,-306 2165,-306 2165,-312 2159,-318 2153,-318"/> +<text text-anchor="middle" x="2056" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go-digest</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->github.com/opencontainers/go-digest --> +<g id="edge14" class="edge"> +<title>github.com/containerd/containerd/content->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M1936.6218,-563.8866C1908.715,-552.1503 1878.2214,-533.7473 1862,-506 1853.9249,-492.1872 1855.3939,-484.5725 1862,-470 1893.6148,-400.2607 1969.5416,-347.8431 2016.4844,-320.7194"/> +<polygon fill="#000000" stroke="#000000" points="2017.4632,-322.1756 2020.9343,-318.1737 2015.7252,-319.1376 2017.4632,-322.1756"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="node22" class="node"> +<title>github.com/opencontainers/image-spec/specs-go/v1</title> +<g id="a_node22"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image-spec/specs-go/v1" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M1540.5,-506C1540.5,-506 1265.5,-506 1265.5,-506 1259.5,-506 1253.5,-500 1253.5,-494 1253.5,-494 1253.5,-482 1253.5,-482 1253.5,-476 1259.5,-470 1265.5,-470 1265.5,-470 1540.5,-470 1540.5,-470 1546.5,-470 1552.5,-476 1552.5,-482 1552.5,-482 1552.5,-494 1552.5,-494 1552.5,-500 1546.5,-506 1540.5,-506"/> +<text text-anchor="middle" x="1403" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go/v1</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge15" class="edge"> +<title>github.com/containerd/containerd/content->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M1883.1737,-563.9871C1779.5464,-547.5881 1627.1084,-523.465 1521.9193,-506.8189"/> +<polygon fill="#000000" stroke="#000000" points="1522.1585,-505.085 1516.9464,-506.0319 1521.6113,-508.542 1522.1585,-505.085"/> +</g> +<!-- github.com/pkg/errors --> +<g id="node23" class="node"> +<title>github.com/pkg/errors</title> +<g id="a_node23"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M5522,-412C5522,-412 5408,-412 5408,-412 5402,-412 5396,-406 5396,-400 5396,-400 5396,-388 5396,-388 5396,-382 5402,-376 5408,-376 5408,-376 5522,-376 5522,-376 5528,-376 5534,-382 5534,-388 5534,-388 5534,-400 5534,-400 5534,-406 5528,-412 5522,-412"/> +<text text-anchor="middle" x="5465" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->github.com/pkg/errors --> +<g id="edge16" class="edge"> +<title>github.com/containerd/containerd/content->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2119.1552,-575.378C2674.1481,-545.2919 4950.03,-421.9165 5390.8598,-398.0191"/> +<polygon fill="#000000" stroke="#000000" points="5391.0632,-399.7608 5395.9611,-397.7426 5390.8737,-396.2659 5391.0632,-399.7608"/> +</g> +<!-- io/ioutil --> +<g id="node24" class="node"> +<title>io/ioutil</title> +<g id="a_node24"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M105.5,-36C105.5,-36 70.5,-36 70.5,-36 64.5,-36 58.5,-30 58.5,-24 58.5,-24 58.5,-12 58.5,-12 58.5,-6 64.5,0 70.5,0 70.5,0 105.5,0 105.5,0 111.5,0 117.5,-6 117.5,-12 117.5,-12 117.5,-24 117.5,-24 117.5,-30 111.5,-36 105.5,-36"/> +<text text-anchor="middle" x="88" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->io/ioutil --> +<g id="edge18" class="edge"> +<title>github.com/containerd/containerd/content->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1874.9668,-576.2824C1724.3992,-567.7358 1461.0731,-547.9156 1239,-506 934.6672,-448.5581 861.3836,-417.2175 568,-318 461.0476,-281.8304 428.832,-282.057 332,-224 240.797,-169.318 149.2829,-80.9301 109.1253,-40.0444"/> +<polygon fill="#000000" stroke="#000000" points="110.149,-38.5883 105.4012,-36.2387 107.6475,-41.0363 110.149,-38.5883"/> +</g> +<!-- math/rand --> +<g id="node25" class="node"> +<title>math/rand</title> +<g id="a_node25"><a xlink:href="https://godoc.org/math/rand" xlink:title="math/rand" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1937,-506C1937,-506 1889,-506 1889,-506 1883,-506 1877,-500 1877,-494 1877,-494 1877,-482 1877,-482 1877,-476 1883,-470 1889,-470 1889,-470 1937,-470 1937,-470 1943,-470 1949,-476 1949,-482 1949,-482 1949,-494 1949,-494 1949,-500 1943,-506 1937,-506"/> +<text text-anchor="middle" x="1913" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/rand</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->math/rand --> +<g id="edge19" class="edge"> +<title>github.com/containerd/containerd/content->math/rand</title> +<path fill="none" stroke="#000000" d="M1980.804,-563.8759C1967.0915,-548.531 1947.3877,-526.4815 1932.7515,-510.1029"/> +<polygon fill="#000000" stroke="#000000" points="1933.7829,-508.6307 1929.1463,-506.0685 1931.1731,-510.9629 1933.7829,-508.6307"/> +</g> +<!-- time --> +<g id="node26" class="node"> +<title>time</title> +<g id="a_node26"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1165,-36C1165,-36 1135,-36 1135,-36 1129,-36 1123,-30 1123,-24 1123,-24 1123,-12 1123,-12 1123,-6 1129,0 1135,0 1135,0 1165,0 1165,0 1171,0 1177,-6 1177,-12 1177,-12 1177,-24 1177,-24 1177,-30 1171,-36 1165,-36"/> +<text text-anchor="middle" x="1150" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/content->time --> +<g id="edge21" class="edge"> +<title>github.com/containerd/containerd/content->time</title> +<path fill="none" stroke="#000000" d="M1957.6231,-563.9006C1927.4327,-549.4905 1885.1252,-528.1289 1850,-506 1768.925,-454.9229 1766.3052,-413.2051 1678,-376 1519.3439,-309.1544 1438.6175,-412.9426 1295,-318 1196.1619,-252.6601 1162.6623,-99.839 1153.2179,-41.3442"/> +<polygon fill="#000000" stroke="#000000" points="1154.9321,-40.9786 1152.4274,-36.3106 1151.4744,-41.5216 1154.9321,-40.9786"/> +</g> +<!-- github.com/containerd/containerd/errdefs->context --> +<g id="edge22" class="edge"> +<title>github.com/containerd/containerd/errdefs->context</title> +<path fill="none" stroke="#000000" d="M3132.887,-479.02C3033.5864,-469.2886 2890.3056,-449.7779 2770,-412 2735.1057,-401.0426 2729.6095,-390.4251 2696,-376 2628.4087,-346.9899 2591.864,-371.1321 2541,-318 2491.3694,-266.1563 2478.5014,-177.1309 2475.1662,-135.1689"/> +<polygon fill="#000000" stroke="#000000" points="2476.9018,-134.9073 2474.7885,-130.0497 2473.4113,-135.1649 2476.9018,-134.9073"/> +</g> +<!-- github.com/containerd/containerd/errdefs->github.com/pkg/errors --> +<g id="edge23" class="edge"> +<title>github.com/containerd/containerd/errdefs->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M3375.2316,-482.8459C3778.3763,-465.7063 5066.7743,-410.9304 5390.5994,-397.1631"/> +<polygon fill="#000000" stroke="#000000" points="5390.8397,-398.9046 5395.7608,-396.9437 5390.691,-395.4077 5390.8397,-398.9046"/> +</g> +<!-- google.golang.org/grpc/codes --> +<g id="node27" class="node"> +<title>google.golang.org/grpc/codes</title> +<g id="a_node27"><a xlink:href="https://godoc.org/google.golang.org/grpc/codes" xlink:title="google.golang.org/grpc/codes" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M1360,-130C1360,-130 1206,-130 1206,-130 1200,-130 1194,-124 1194,-118 1194,-118 1194,-106 1194,-106 1194,-100 1200,-94 1206,-94 1206,-94 1360,-94 1360,-94 1366,-94 1372,-100 1372,-106 1372,-106 1372,-118 1372,-118 1372,-124 1366,-130 1360,-130"/> +<text text-anchor="middle" x="1283" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/codes</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/errdefs->google.golang.org/grpc/codes --> +<g id="edge24" class="edge"> +<title>github.com/containerd/containerd/errdefs->google.golang.org/grpc/codes</title> +<path fill="none" stroke="#000000" d="M3132.6544,-479.2349C2715.2007,-448.8354 1361.9526,-348.1512 1328,-318 1275.3941,-271.284 1276.4122,-178.609 1280.1883,-135.3099"/> +<polygon fill="#000000" stroke="#000000" points="1281.9354,-135.4221 1280.6591,-130.2807 1278.4506,-135.0958 1281.9354,-135.4221"/> +</g> +<!-- google.golang.org/grpc/status --> +<g id="node28" class="node"> +<title>google.golang.org/grpc/status</title> +<g id="a_node28"><a xlink:href="https://godoc.org/google.golang.org/grpc/status" xlink:title="google.golang.org/grpc/status" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M3331,-412C3331,-412 3177,-412 3177,-412 3171,-412 3165,-406 3165,-400 3165,-400 3165,-388 3165,-388 3165,-382 3171,-376 3177,-376 3177,-376 3331,-376 3331,-376 3337,-376 3343,-382 3343,-388 3343,-388 3343,-400 3343,-400 3343,-406 3337,-412 3331,-412"/> +<text text-anchor="middle" x="3254" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/status</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/errdefs->google.golang.org/grpc/status --> +<g id="edge25" class="edge"> +<title>github.com/containerd/containerd/errdefs->google.golang.org/grpc/status</title> +<path fill="none" stroke="#000000" d="M3254,-469.8759C3254,-454.9211 3254,-433.5983 3254,-417.3629"/> +<polygon fill="#000000" stroke="#000000" points="3255.7501,-417.0685 3254,-412.0685 3252.2501,-417.0685 3255.7501,-417.0685"/> +</g> +<!-- strings --> +<g id="node29" class="node"> +<title>strings</title> +<g id="a_node29"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M6629,-36C6629,-36 6599,-36 6599,-36 6593,-36 6587,-30 6587,-24 6587,-24 6587,-12 6587,-12 6587,-6 6593,0 6599,0 6599,0 6629,0 6629,0 6635,0 6641,-6 6641,-12 6641,-12 6641,-24 6641,-24 6641,-30 6635,-36 6629,-36"/> +<text text-anchor="middle" x="6614" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/errdefs->strings --> +<g id="edge26" class="edge"> +<title>github.com/containerd/containerd/errdefs->strings</title> +<path fill="none" stroke="#000000" d="M3375.1762,-487.1805C3873.2089,-483.4687 5748.4723,-466.003 6008,-412 6117.5254,-389.2097 6376.4903,-277.0193 6475,-224 6539.7013,-189.1768 6576.0056,-194.6089 6611,-130 6625.8998,-102.4911 6623.2617,-65.3396 6619.2414,-41.52"/> +<polygon fill="#000000" stroke="#000000" points="6620.9198,-40.9656 6618.3125,-36.3543 6617.4751,-41.5851 6620.9198,-40.9656"/> +</g> +<!-- github.com/opencontainers/go-digest->crypto --> +<g id="edge171" class="edge"> +<title>github.com/opencontainers/go-digest->crypto</title> +<path fill="none" stroke="#000000" d="M2054.6503,-281.8759C2053.5367,-266.9211 2051.9488,-245.5983 2050.7398,-229.3629"/> +<polygon fill="#000000" stroke="#000000" points="2052.4621,-228.9247 2050.3455,-224.0685 2048.9718,-229.1847 2052.4621,-228.9247"/> +</g> +<!-- github.com/opencontainers/go-digest->fmt --> +<g id="edge172" class="edge"> +<title>github.com/opencontainers/go-digest->fmt</title> +<path fill="none" stroke="#000000" d="M2064.262,-281.9692C2086.8443,-232.8516 2148.4873,-99.9413 2157,-94 2214.9068,-53.5846 2719.9028,-26.1757 2854.2748,-19.5526"/> +<polygon fill="#000000" stroke="#000000" points="2854.7384,-21.2821 2859.6467,-19.2892 2854.567,-17.7863 2854.7384,-21.2821"/> +</g> +<!-- github.com/opencontainers/go-digest->io --> +<g id="edge174" class="edge"> +<title>github.com/opencontainers/go-digest->io</title> +<path fill="none" stroke="#000000" d="M1946.8622,-283.4425C1828.2455,-265.0424 1651.5748,-236.3031 1622,-224 1529.5511,-185.5412 1530.4369,-132.4876 1438,-94 1300.5256,-36.7603 1254.2945,-63.8126 1108,-36 1089.535,-32.4895 1068.95,-28.2945 1052.3346,-24.8378"/> +<polygon fill="#000000" stroke="#000000" points="1052.5643,-23.0981 1047.3123,-23.7903 1051.8496,-26.5244 1052.5643,-23.0981"/> +</g> +<!-- github.com/opencontainers/go-digest->strings --> +<g id="edge176" class="edge"> +<title>github.com/opencontainers/go-digest->strings</title> +<path fill="none" stroke="#000000" d="M2165.0049,-298.1793C2701.4226,-289.1093 5037.956,-248.2757 5109,-224 5134.5719,-215.2621 5132.5236,-197.0125 5158,-188 5419.9471,-95.3342 6145.0529,-222.6658 6407,-130 6432.4764,-120.9875 6432.8354,-107.9167 6456,-94 6498.0289,-68.7501 6549.721,-45.1863 6582.2764,-31.1834"/> +<polygon fill="#000000" stroke="#000000" points="6583.0298,-32.7646 6586.9373,-29.1877 6581.6521,-29.5471 6583.0298,-32.7646"/> +</g> +<!-- regexp --> +<g id="node38" class="node"> +<title>regexp</title> +<g id="a_node38"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5461,-224C5461,-224 5431,-224 5431,-224 5425,-224 5419,-218 5419,-212 5419,-212 5419,-200 5419,-200 5419,-194 5425,-188 5431,-188 5431,-188 5461,-188 5461,-188 5467,-188 5473,-194 5473,-200 5473,-200 5473,-212 5473,-212 5473,-218 5467,-224 5461,-224"/> +<text text-anchor="middle" x="5446" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/go-digest->regexp --> +<g id="edge175" class="edge"> +<title>github.com/opencontainers/go-digest->regexp</title> +<path fill="none" stroke="#000000" d="M2165.0208,-298.8542C2564.825,-294.3286 3988.3012,-275.3653 5161,-224 5252.7011,-219.9834 5360.8281,-212.3619 5413.6596,-208.4505"/> +<polygon fill="#000000" stroke="#000000" points="5414.0477,-210.1766 5418.9043,-208.061 5413.7884,-206.6862 5414.0477,-210.1766"/> +</g> +<!-- hash --> +<g id="node61" class="node"> +<title>hash</title> +<g id="a_node61"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1981,-224C1981,-224 1951,-224 1951,-224 1945,-224 1939,-218 1939,-212 1939,-212 1939,-200 1939,-200 1939,-194 1945,-188 1951,-188 1951,-188 1981,-188 1981,-188 1987,-188 1993,-194 1993,-200 1993,-200 1993,-212 1993,-212 1993,-218 1987,-224 1981,-224"/> +<text text-anchor="middle" x="1966" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/go-digest->hash --> +<g id="edge173" class="edge"> +<title>github.com/opencontainers/go-digest->hash</title> +<path fill="none" stroke="#000000" d="M2038.6471,-281.8759C2023.8307,-266.401 2002.4857,-244.1073 1986.7648,-227.6877"/> +<polygon fill="#000000" stroke="#000000" points="1988.0216,-226.4698 1983.2996,-224.0685 1985.4935,-228.8903 1988.0216,-226.4698"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest --> +<g id="edge178" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M1424.1623,-469.9086C1455.0921,-444.5238 1515.4618,-398.884 1575,-376 1592.0312,-369.4539 1804.8774,-337.1682 1941.6634,-316.8398"/> +<polygon fill="#000000" stroke="#000000" points="1942.2132,-318.5274 1946.9017,-316.0616 1941.6988,-315.0654 1942.2132,-318.5274"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->time --> +<g id="edge180" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->time</title> +<path fill="none" stroke="#000000" d="M1253.2425,-470.0197C1199.6674,-458.9087 1140.5754,-440.9629 1092,-412 995.243,-354.3089 962.482,-329.1255 922,-224 883.434,-123.8499 1046.2762,-53.8911 1117.7978,-28.5184"/> +<polygon fill="#000000" stroke="#000000" points="1118.6466,-30.0753 1122.7855,-26.7689 1117.488,-26.7726 1118.6466,-30.0753"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go --> +<g id="node50" class="node"> +<title>github.com/opencontainers/image-spec/specs-go</title> +<g id="a_node50"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image-spec/specs-go" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M1375.5,-412C1375.5,-412 1118.5,-412 1118.5,-412 1112.5,-412 1106.5,-406 1106.5,-400 1106.5,-400 1106.5,-388 1106.5,-388 1106.5,-382 1112.5,-376 1118.5,-376 1118.5,-376 1375.5,-376 1375.5,-376 1381.5,-376 1387.5,-382 1387.5,-388 1387.5,-388 1387.5,-400 1387.5,-400 1387.5,-406 1381.5,-412 1375.5,-412"/> +<text text-anchor="middle" x="1247" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go --> +<g id="edge179" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go</title> +<path fill="none" stroke="#000000" d="M1372.9216,-469.8759C1346.7004,-454.0759 1308.6824,-431.1676 1281.288,-414.6607"/> +<polygon fill="#000000" stroke="#000000" points="1282.1719,-413.1502 1276.986,-412.0685 1280.3654,-416.148 1282.1719,-413.1502"/> +</g> +<!-- github.com/pkg/errors->fmt --> +<g id="edge181" class="edge"> +<title>github.com/pkg/errors->fmt</title> +<path fill="none" stroke="#000000" d="M5395.898,-392.9764C4999.7014,-386.9744 3032.5298,-355.3296 2983,-318 2893.7342,-250.7222 2885.8151,-99.6195 2886.2489,-41.4463"/> +<polygon fill="#000000" stroke="#000000" points="2888.0028,-41.1338 2886.3145,-36.1126 2884.503,-41.0907 2888.0028,-41.1338"/> +</g> +<!-- github.com/pkg/errors->io --> +<g id="edge182" class="edge"> +<title>github.com/pkg/errors->io</title> +<path fill="none" stroke="#000000" d="M5395.9615,-393.6255C4900.328,-390.8096 1898.4662,-371.5806 1715,-318 1638.0279,-295.5206 1627.4758,-268.844 1561,-224 1558.4899,-222.3067 1388.7624,-95.2397 1386,-94 1382.1359,-92.2659 1141.004,-42.7832 1052.2928,-24.6108"/> +<polygon fill="#000000" stroke="#000000" points="1052.4254,-22.8517 1047.1759,-23.5627 1051.723,-26.2805 1052.4254,-22.8517"/> +</g> +<!-- github.com/pkg/errors->strings --> +<g id="edge185" class="edge"> +<title>github.com/pkg/errors->strings</title> +<path fill="none" stroke="#000000" d="M5534.2326,-377.154C5764.6537,-321.0134 6492.3732,-143.0889 6514,-130 6551.5248,-107.2895 6582.9231,-66.0466 6600.0369,-40.4451"/> +<polygon fill="#000000" stroke="#000000" points="6601.5679,-41.3025 6602.8621,-36.1656 6598.647,-39.3742 6601.5679,-41.3025"/> +</g> +<!-- runtime --> +<g id="node39" class="node"> +<title>runtime</title> +<g id="a_node39"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7014.5,-36C7014.5,-36 6979.5,-36 6979.5,-36 6973.5,-36 6967.5,-30 6967.5,-24 6967.5,-24 6967.5,-12 6967.5,-12 6967.5,-6 6973.5,0 6979.5,0 6979.5,0 7014.5,0 7014.5,0 7020.5,0 7026.5,-6 7026.5,-12 7026.5,-12 7026.5,-24 7026.5,-24 7026.5,-30 7020.5,-36 7014.5,-36"/> +<text text-anchor="middle" x="6997" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text> +</a> +</g> +</g> +<!-- github.com/pkg/errors->runtime --> +<g id="edge184" class="edge"> +<title>github.com/pkg/errors->runtime</title> +<path fill="none" stroke="#000000" d="M5534.2114,-383.597C5614.772,-370.8712 5751.523,-347.3538 5867,-318 5917.6745,-305.1188 5928.4786,-295.4692 5979,-282 6098.435,-250.1582 6130.929,-253.3525 6251,-224 6527.2287,-156.4734 6856.5962,-59.8037 6962.4471,-28.3297"/> +<polygon fill="#000000" stroke="#000000" points="6963.0243,-29.9838 6967.3176,-26.8806 6962.0261,-26.6292 6963.0243,-29.9838"/> +</g> +<!-- path --> +<g id="node42" class="node"> +<title>path</title> +<g id="a_node42"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M6036,-318C6036,-318 6006,-318 6006,-318 6000,-318 5994,-312 5994,-306 5994,-306 5994,-294 5994,-294 5994,-288 6000,-282 6006,-282 6006,-282 6036,-282 6036,-282 6042,-282 6048,-288 6048,-294 6048,-294 6048,-306 6048,-306 6048,-312 6042,-318 6036,-318"/> +<text text-anchor="middle" x="6021" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text> +</a> +</g> +</g> +<!-- github.com/pkg/errors->path --> +<g id="edge183" class="edge"> +<title>github.com/pkg/errors->path</title> +<path fill="none" stroke="#000000" d="M5534.3585,-388.1067C5632.8353,-378.727 5818.2984,-357.3151 5972,-318 5977.5122,-316.59 5983.2678,-314.7891 5988.8159,-312.8737"/> +<polygon fill="#000000" stroke="#000000" points="5989.7132,-314.4122 5993.8397,-311.0904 5988.5424,-311.1138 5989.7132,-314.4122"/> +</g> +<!-- google.golang.org/grpc/codes->fmt --> +<g id="edge224" class="edge"> +<title>google.golang.org/grpc/codes->fmt</title> +<path fill="none" stroke="#000000" d="M1372.0294,-101.6826C1398.0988,-98.9028 1426.6686,-96.0903 1453,-94 2012.9986,-49.5443 2698.1532,-24.4678 2854.294,-19.097"/> +<polygon fill="#000000" stroke="#000000" points="2854.6871,-20.8347 2859.6242,-18.9143 2854.5671,-17.3367 2854.6871,-20.8347"/> +</g> +<!-- google.golang.org/grpc/codes->strconv --> +<g id="edge225" class="edge"> +<title>google.golang.org/grpc/codes->strconv</title> +<path fill="none" stroke="#000000" d="M1193.8595,-95.1404C1081.8778,-73.9608 895.2191,-38.6571 819.4715,-24.3306"/> +<polygon fill="#000000" stroke="#000000" points="819.45,-22.5456 814.2119,-23.3359 818.7996,-25.9846 819.45,-22.5456"/> +</g> +<!-- google.golang.org/grpc/status->context --> +<g id="edge236" class="edge"> +<title>google.golang.org/grpc/status->context</title> +<path fill="none" stroke="#000000" d="M3164.9707,-389.2956C3032.8474,-380.9995 2792.4411,-360.4695 2718,-318 2653.7318,-281.3343 2670.749,-235.4797 2614,-188 2581.0809,-160.4578 2536.6218,-138.378 2506.7134,-125.26"/> +<polygon fill="#000000" stroke="#000000" points="2507.3256,-123.618 2502.0422,-123.2321 2505.9318,-126.8285 2507.3256,-123.618"/> +</g> +<!-- google.golang.org/grpc/status->errors --> +<g id="edge237" class="edge"> +<title>google.golang.org/grpc/status->errors</title> +<path fill="none" stroke="#000000" d="M3343.2399,-392.8388C3587.0083,-388.9557 4251.3796,-373.3562 4332,-318 4382.2849,-283.473 4386.1409,-248.1486 4376,-188 4366.8301,-133.6104 4342.2458,-73.3834 4327.8702,-41.2405"/> +<polygon fill="#000000" stroke="#000000" points="4329.3056,-40.1665 4325.6546,-36.3281 4326.1151,-41.6055 4329.3056,-40.1665"/> +</g> +<!-- google.golang.org/grpc/status->fmt --> +<g id="edge238" class="edge"> +<title>google.golang.org/grpc/status->fmt</title> +<path fill="none" stroke="#000000" d="M3164.7891,-385.0252C3040.7909,-371.6391 2828.8912,-345.1016 2806,-318 2741.7598,-241.9441 2766.4425,-185.3593 2806,-94 2816.1863,-70.4743 2837.6378,-50.8867 2855.9068,-37.4917"/> +<polygon fill="#000000" stroke="#000000" points="2856.9498,-38.8972 2859.991,-34.5597 2854.9086,-36.054 2856.9498,-38.8972"/> +</g> +<!-- google.golang.org/grpc/status->google.golang.org/grpc/codes --> +<g id="edge242" class="edge"> +<title>google.golang.org/grpc/status->google.golang.org/grpc/codes</title> +<path fill="none" stroke="#000000" d="M3164.7863,-391.2316C2851.2943,-381.2971 1811.186,-346.5195 1664,-318 1632.8139,-311.9572 1420.8725,-240.9397 1394,-224 1355.2506,-199.5735 1319.7363,-159.2322 1299.7815,-134.2144"/> +<polygon fill="#000000" stroke="#000000" points="1300.9499,-132.8704 1296.4769,-130.0324 1298.2038,-135.0404 1300.9499,-132.8704"/> +</g> +<!-- github.com/golang/protobuf/proto --> +<g id="node51" class="node"> +<title>github.com/golang/protobuf/proto</title> +<g id="a_node51"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M4183,-130C4183,-130 4005,-130 4005,-130 3999,-130 3993,-124 3993,-118 3993,-118 3993,-106 3993,-106 3993,-100 3999,-94 4005,-94 4005,-94 4183,-94 4183,-94 4189,-94 4195,-100 4195,-106 4195,-106 4195,-118 4195,-118 4195,-124 4189,-130 4183,-130"/> +<text text-anchor="middle" x="4094" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/status->github.com/golang/protobuf/proto --> +<g id="edge239" class="edge"> +<title>google.golang.org/grpc/status->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3343.0024,-392.0198C3578.8082,-386.1359 4207.127,-366.0808 4287,-318 4341.3606,-285.2768 4383.8381,-238.9329 4346,-188 4327.9099,-163.6493 4260.8032,-144.1145 4200.3879,-130.95"/> +<polygon fill="#000000" stroke="#000000" points="4200.5663,-129.1983 4195.3097,-129.8549 4199.8284,-132.6197 4200.5663,-129.1983"/> +</g> +<!-- github.com/golang/protobuf/ptypes --> +<g id="node57" class="node"> +<title>github.com/golang/protobuf/ptypes</title> +<g id="a_node57"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes" xlink:title="github.com/golang/protobuf/ptypes" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3683.5,-318C3683.5,-318 3498.5,-318 3498.5,-318 3492.5,-318 3486.5,-312 3486.5,-306 3486.5,-306 3486.5,-294 3486.5,-294 3486.5,-288 3492.5,-282 3498.5,-282 3498.5,-282 3683.5,-282 3683.5,-282 3689.5,-282 3695.5,-288 3695.5,-294 3695.5,-294 3695.5,-306 3695.5,-306 3695.5,-312 3689.5,-318 3683.5,-318"/> +<text text-anchor="middle" x="3591" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/status->github.com/golang/protobuf/ptypes --> +<g id="edge240" class="edge"> +<title>google.golang.org/grpc/status->github.com/golang/protobuf/ptypes</title> +<path fill="none" stroke="#000000" d="M3318.5782,-375.9871C3376.6702,-359.7834 3461.7991,-336.0382 3521.3897,-319.4165"/> +<polygon fill="#000000" stroke="#000000" points="3522.0077,-321.061 3526.3537,-318.0319 3521.0673,-317.6897 3522.0077,-321.061"/> +</g> +<!-- google.golang.org/genproto/googleapis/rpc/status --> +<g id="node64" class="node"> +<title>google.golang.org/genproto/googleapis/rpc/status</title> +<g id="a_node64"><a xlink:href="https://godoc.org/google.golang.org/genproto/googleapis/rpc/status" xlink:title="google.golang.org/genproto/googleapis/rpc/status" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3999.5,-318C3999.5,-318 3738.5,-318 3738.5,-318 3732.5,-318 3726.5,-312 3726.5,-306 3726.5,-306 3726.5,-294 3726.5,-294 3726.5,-288 3732.5,-282 3738.5,-282 3738.5,-282 3999.5,-282 3999.5,-282 4005.5,-282 4011.5,-288 4011.5,-294 4011.5,-294 4011.5,-306 4011.5,-306 4011.5,-312 4005.5,-318 3999.5,-318"/> +<text text-anchor="middle" x="3869" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/genproto/googleapis/rpc/status</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/status->google.golang.org/genproto/googleapis/rpc/status --> +<g id="edge241" class="edge"> +<title>google.golang.org/grpc/status->google.golang.org/genproto/googleapis/rpc/status</title> +<path fill="none" stroke="#000000" d="M3343.0976,-380.3818C3449.1289,-364.1754 3626.7644,-337.0246 3745.8794,-318.8184"/> +<polygon fill="#000000" stroke="#000000" points="3746.4743,-320.4979 3751.1525,-318.0125 3745.9455,-317.0381 3746.4743,-320.4979"/> +</g> +<!-- google.golang.org/grpc/internal --> +<g id="node67" class="node"> +<title>google.golang.org/grpc/internal</title> +<g id="a_node67"><a xlink:href="https://godoc.org/google.golang.org/grpc/internal" xlink:title="google.golang.org/grpc/internal" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1906,-318C1906,-318 1742,-318 1742,-318 1736,-318 1730,-312 1730,-306 1730,-306 1730,-294 1730,-294 1730,-288 1736,-282 1742,-282 1742,-282 1906,-282 1906,-282 1912,-282 1918,-288 1918,-294 1918,-294 1918,-306 1918,-306 1918,-312 1912,-318 1906,-318"/> +<text text-anchor="middle" x="1824" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/internal</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/status->google.golang.org/grpc/internal --> +<g id="edge243" class="edge"> +<title>google.golang.org/grpc/status->google.golang.org/grpc/internal</title> +<path fill="none" stroke="#000000" d="M3164.7476,-392.1849C2951.1801,-387.1198 2394.082,-369.6371 1932,-318 1929.1663,-317.6833 1926.294,-317.342 1923.3971,-316.9799"/> +<polygon fill="#000000" stroke="#000000" points="1923.3418,-315.2085 1918.1595,-316.3067 1922.8956,-318.68 1923.3418,-315.2085"/> +</g> +<!-- github.com/containerd/containerd/images --> +<g id="node30" class="node"> +<title>github.com/containerd/containerd/images</title> +<g id="a_node30"><a xlink:href="https://godoc.org/github.com/containerd/containerd/images" xlink:title="github.com/containerd/containerd/images" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3722.5,-694C3722.5,-694 3503.5,-694 3503.5,-694 3497.5,-694 3491.5,-688 3491.5,-682 3491.5,-682 3491.5,-670 3491.5,-670 3491.5,-664 3497.5,-658 3503.5,-658 3503.5,-658 3722.5,-658 3722.5,-658 3728.5,-658 3734.5,-664 3734.5,-670 3734.5,-670 3734.5,-682 3734.5,-682 3734.5,-688 3728.5,-694 3722.5,-694"/> +<text text-anchor="middle" x="3613" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/images</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/images->context --> +<g id="edge27" class="edge"> +<title>github.com/containerd/containerd/images->context</title> +<path fill="none" stroke="#000000" d="M3554.1735,-657.8645C3522.0815,-645.6168 3483.4246,-626.7461 3456,-600 3409.466,-554.6174 3442.5167,-506.891 3389,-470 3294.1781,-404.6359 2986.0649,-438.5539 2874,-412 2844.6165,-405.0375 2642.9962,-336.3316 2619,-318 2552.4056,-267.1259 2504.2667,-176.999 2484.2649,-134.8826"/> +<polygon fill="#000000" stroke="#000000" points="2485.7895,-134.0122 2482.0782,-130.2321 2482.6221,-135.5015 2485.7895,-134.0122"/> +</g> +<!-- github.com/containerd/containerd/images->encoding/json --> +<g id="edge28" class="edge"> +<title>github.com/containerd/containerd/images->encoding/json</title> +<path fill="none" stroke="#000000" d="M3734.5923,-675.0239C3980.838,-671.8318 4530.1746,-658.2013 4708,-600 4736.4765,-590.6798 4737.6511,-576.2398 4765,-564 4850.6465,-525.6696 4878.5614,-534.3776 4968,-506 5013.2679,-491.6372 5743.1045,-259.1873 5775,-224 5822.1683,-171.9636 5826.7979,-83.0328 5826.0988,-41.1315"/> +<polygon fill="#000000" stroke="#000000" points="5827.8458,-40.9788 5825.9827,-36.0198 5824.3467,-41.0583 5827.8458,-40.9788"/> +</g> +<!-- github.com/containerd/containerd/images->fmt --> +<g id="edge29" class="edge"> +<title>github.com/containerd/containerd/images->fmt</title> +<path fill="none" stroke="#000000" d="M3603.7904,-657.893C3575.4351,-604.2377 3483.6486,-445.5089 3357,-376 3194.5074,-286.8187 3094.034,-426.8436 2944,-318 2855.8997,-254.0867 2872.4469,-99.7406 2882.4557,-41.116"/> +<polygon fill="#000000" stroke="#000000" points="2884.1997,-41.302 2883.341,-36.0746 2880.7525,-40.6966 2884.1997,-41.302"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/containerd/containerd/log --> +<g id="edge32" class="edge"> +<title>github.com/containerd/containerd/images->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M3734.8909,-672.8566C3981.6171,-665.5885 4522.8612,-644.7126 4594,-600 4691.4556,-538.7465 4634.7869,-448.7366 4724,-376 4757.8157,-348.4297 4802.0716,-330.6908 4842.518,-319.3564"/> +<polygon fill="#000000" stroke="#000000" points="4843.0736,-321.0187 4847.4307,-318.0057 4842.1456,-317.6439 4843.0736,-321.0187"/> +</g> +<!-- github.com/containerd/containerd/images->io --> +<g id="edge39" class="edge"> +<title>github.com/containerd/containerd/images->io</title> +<path fill="none" stroke="#000000" d="M3491.196,-674.4534C3094.4414,-668.989 1841.7309,-648.262 1440,-600 1090.9143,-558.0626 708,-651.5957 708,-300 708,-300 708,-300 708,-206 708,-76.3582 908.0683,-33.7909 987.7845,-21.9756"/> +<polygon fill="#000000" stroke="#000000" points="988.1601,-23.6895 992.8578,-21.2412 987.6587,-20.2256 988.1601,-23.6895"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/containerd/containerd/content --> +<g id="edge30" class="edge"> +<title>github.com/containerd/containerd/images->github.com/containerd/containerd/content</title> +<path fill="none" stroke="#000000" d="M3491.2063,-668.9155C3193.0397,-651.5716 2428.6826,-607.1103 2124.5811,-589.4212"/> +<polygon fill="#000000" stroke="#000000" points="2124.3454,-587.6546 2119.2522,-589.1112 2124.1421,-591.1487 2124.3454,-587.6546"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/containerd/containerd/errdefs --> +<g id="edge31" class="edge"> +<title>github.com/containerd/containerd/images->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M3543.9259,-657.9696C3500.2798,-645.0794 3443.5392,-625.5075 3397,-600 3350.6113,-574.5749 3303.6153,-534.3797 3276.7265,-509.6884"/> +<polygon fill="#000000" stroke="#000000" points="3277.7682,-508.2682 3272.9077,-506.1615 3275.3935,-510.8395 3277.7682,-508.2682"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/opencontainers/go-digest --> +<g id="edge34" class="edge"> +<title>github.com/containerd/containerd/images->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M3491.3502,-661.3216C3256.3551,-630.849 2725.462,-551.6624 2298,-412 2222.6955,-387.3962 2139.934,-345.6408 2093.3326,-320.6574"/> +<polygon fill="#000000" stroke="#000000" points="2093.8933,-318.9719 2088.6609,-318.1441 2092.235,-322.0541 2093.8933,-318.9719"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge35" class="edge"> +<title>github.com/containerd/containerd/images->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M3491.4591,-673.8472C3148.1824,-667.2714 2175.7975,-645.0934 1860,-600 1716.7417,-579.5438 1552.9136,-533.7667 1465.5844,-507.5083"/> +<polygon fill="#000000" stroke="#000000" points="1466.0064,-505.8077 1460.7141,-506.0402 1464.9963,-509.1588 1466.0064,-505.8077"/> +</g> +<!-- github.com/containerd/containerd/images->github.com/pkg/errors --> +<g id="edge36" class="edge"> +<title>github.com/containerd/containerd/images->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M3734.703,-670.8191C3989.5107,-659.5089 4571.7578,-631.1157 4768,-600 4935.575,-573.4297 4975.3866,-554.4175 5138,-506 5235.392,-477.0019 5347.0185,-437.3611 5411.4338,-413.8456"/> +<polygon fill="#000000" stroke="#000000" points="5412.2757,-415.4012 5416.371,-412.041 5411.0741,-412.1139 5412.2757,-415.4012"/> +</g> +<!-- github.com/containerd/containerd/images->time --> +<g id="edge42" class="edge"> +<title>github.com/containerd/containerd/images->time</title> +<path fill="none" stroke="#000000" d="M3491.3994,-673.9778C3124.1018,-667.4347 2029.6154,-644.6294 1676,-600 1478.8989,-575.1241 1428.0919,-566.9255 1239,-506 1021.1423,-435.8062 767,-528.8867 767,-300 767,-300 767,-300 767,-206 767,-128.1229 1025.4001,-51.3413 1117.7082,-26.3972"/> +<polygon fill="#000000" stroke="#000000" points="1118.3884,-28.0266 1122.7626,-25.0384 1117.4797,-24.6466 1118.3884,-28.0266"/> +</g> +<!-- github.com/containerd/containerd/images->strings --> +<g id="edge41" class="edge"> +<title>github.com/containerd/containerd/images->strings</title> +<path fill="none" stroke="#000000" d="M3734.7956,-673.5673C4144.691,-665.0866 5458.9257,-635.4084 5646,-600 5867.4609,-558.0831 6418.9011,-373.022 6588,-224 6628.3063,-188.4792 6646.0859,-180.6498 6664,-130 6675.2966,-98.0605 6653.2405,-62.5623 6634.9244,-40.2647"/> +<polygon fill="#000000" stroke="#000000" points="6636.1137,-38.9599 6631.5567,-36.2587 6633.4346,-41.2121 6636.1137,-38.9599"/> +</g> +<!-- github.com/containerd/containerd/platforms --> +<g id="node31" class="node"> +<title>github.com/containerd/containerd/platforms</title> +<g id="a_node31"><a xlink:href="https://godoc.org/github.com/containerd/containerd/platforms" xlink:title="github.com/containerd/containerd/platforms" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M4567.5,-600C4567.5,-600 4334.5,-600 4334.5,-600 4328.5,-600 4322.5,-594 4322.5,-588 4322.5,-588 4322.5,-576 4322.5,-576 4322.5,-570 4328.5,-564 4334.5,-564 4334.5,-564 4567.5,-564 4567.5,-564 4573.5,-564 4579.5,-570 4579.5,-576 4579.5,-576 4579.5,-588 4579.5,-588 4579.5,-594 4573.5,-600 4567.5,-600"/> +<text text-anchor="middle" x="4451" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/platforms</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/images->github.com/containerd/containerd/platforms --> +<g id="edge33" class="edge"> +<title>github.com/containerd/containerd/images->github.com/containerd/containerd/platforms</title> +<path fill="none" stroke="#000000" d="M3734.5994,-662.7541C3871.5683,-647.777 4100.8089,-622.5383 4298,-600 4304.3525,-599.2739 4310.8529,-598.5265 4317.418,-597.768"/> +<polygon fill="#000000" stroke="#000000" points="4317.6838,-599.499 4322.4496,-597.186 4317.2816,-596.0222 4317.6838,-599.499"/> +</g> +<!-- golang.org/x/sync/errgroup --> +<g id="node32" class="node"> +<title>golang.org/x/sync/errgroup</title> +<g id="a_node32"><a xlink:href="https://godoc.org/golang.org/x/sync/errgroup" xlink:title="golang.org/x/sync/errgroup" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M4942,-506C4942,-506 4802,-506 4802,-506 4796,-506 4790,-500 4790,-494 4790,-494 4790,-482 4790,-482 4790,-476 4796,-470 4802,-470 4802,-470 4942,-470 4942,-470 4948,-470 4954,-476 4954,-482 4954,-482 4954,-494 4954,-494 4954,-500 4948,-506 4942,-506"/> +<text text-anchor="middle" x="4872" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sync/errgroup</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/images->golang.org/x/sync/errgroup --> +<g id="edge37" class="edge"> +<title>github.com/containerd/containerd/images->golang.org/x/sync/errgroup</title> +<path fill="none" stroke="#000000" d="M3734.9512,-673.0492C3970.7669,-666.36 4480.925,-646.8295 4650,-600 4659.6044,-597.3398 4770.8962,-540.2248 4832.154,-508.6112"/> +<polygon fill="#000000" stroke="#000000" points="4833.1969,-510.0423 4836.8371,-506.1937 4831.5914,-506.9322 4833.1969,-510.0423"/> +</g> +<!-- golang.org/x/sync/semaphore --> +<g id="node33" class="node"> +<title>golang.org/x/sync/semaphore</title> +<g id="a_node33"><a xlink:href="https://godoc.org/golang.org/x/sync/semaphore" xlink:title="golang.org/x/sync/semaphore" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M4828.5,-224C4828.5,-224 4675.5,-224 4675.5,-224 4669.5,-224 4663.5,-218 4663.5,-212 4663.5,-212 4663.5,-200 4663.5,-200 4663.5,-194 4669.5,-188 4675.5,-188 4675.5,-188 4828.5,-188 4828.5,-188 4834.5,-188 4840.5,-194 4840.5,-200 4840.5,-200 4840.5,-212 4840.5,-212 4840.5,-218 4834.5,-224 4828.5,-224"/> +<text text-anchor="middle" x="4752" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sync/semaphore</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/images->golang.org/x/sync/semaphore --> +<g id="edge38" class="edge"> +<title>github.com/containerd/containerd/images->golang.org/x/sync/semaphore</title> +<path fill="none" stroke="#000000" d="M3656.9522,-657.8634C3836.6756,-583.7019 4515.0372,-303.781 4703.2874,-226.1009"/> +<polygon fill="#000000" stroke="#000000" points="4704.2243,-227.6075 4708.1787,-224.0825 4702.8892,-224.3721 4704.2243,-227.6075"/> +</g> +<!-- sort --> +<g id="node34" class="node"> +<title>sort</title> +<g id="a_node34"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M6105,-36C6105,-36 6075,-36 6075,-36 6069,-36 6063,-30 6063,-24 6063,-24 6063,-12 6063,-12 6063,-6 6069,0 6075,0 6075,0 6105,0 6105,0 6111,0 6117,-6 6117,-12 6117,-12 6117,-24 6117,-24 6117,-30 6111,-36 6105,-36"/> +<text text-anchor="middle" x="6090" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/images->sort --> +<g id="edge40" class="edge"> +<title>github.com/containerd/containerd/images->sort</title> +<path fill="none" stroke="#000000" d="M3734.7198,-675.4067C4135.7009,-672.8876 5412.7667,-660.2191 5820,-600 6164.9264,-548.9945 7197.618,-457.603 7317,-130 7322.4782,-114.967 7327.9584,-105.6582 7317,-94 7295.899,-71.5516 6314.1461,-27.7481 6122.3096,-19.3946"/> +<polygon fill="#000000" stroke="#000000" points="6122.201,-17.6383 6117.1296,-19.1693 6122.0488,-21.135 6122.201,-17.6383"/> +</g> +<!-- github.com/containerd/containerd/platforms->bufio --> +<g id="edge48" class="edge"> +<title>github.com/containerd/containerd/platforms->bufio</title> +<path fill="none" stroke="#000000" d="M4457.2985,-563.805C4471.5353,-524.9361 4509.5733,-432.7298 4568,-376 4671.5569,-275.4508 4780.5582,-347.6632 4855,-224 4917.1763,-120.7124 4754.837,-164.4686 4687,-130 4635.8697,-104.0202 4582.3241,-63.7418 4551.6765,-39.2274"/> +<polygon fill="#000000" stroke="#000000" points="4552.683,-37.7913 4547.6892,-36.0236 4550.4907,-40.5196 4552.683,-37.7913"/> +</g> +<!-- github.com/containerd/containerd/platforms->github.com/containerd/containerd/log --> +<g id="edge50" class="edge"> +<title>github.com/containerd/containerd/platforms->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M4473.7187,-563.9126C4491.0069,-549.4272 4514.6946,-527.9621 4532,-506 4573.1472,-453.7806 4552.4793,-415.4397 4606,-376 4625.3419,-361.7469 4750.2634,-336.4681 4844.2684,-319.0013"/> +<polygon fill="#000000" stroke="#000000" points="4844.6868,-320.7036 4849.2839,-318.0713 4844.0486,-317.2623 4844.6868,-320.7036"/> +</g> +<!-- github.com/containerd/containerd/platforms->os --> +<g id="edge53" class="edge"> +<title>github.com/containerd/containerd/platforms->os</title> +<path fill="none" stroke="#000000" d="M4440.5305,-563.6051C4403.4981,-500.3088 4271.8066,-289.8069 4103,-188 4009.3996,-131.5498 3956.5746,-188.1171 3864,-130 3828.2877,-107.5803 3799.9092,-66.5233 3784.565,-40.8338"/> +<polygon fill="#000000" stroke="#000000" points="3785.8966,-39.6459 3781.8527,-36.2238 3782.8799,-41.4207 3785.8966,-39.6459"/> +</g> +<!-- github.com/containerd/containerd/platforms->strconv --> +<g id="edge56" class="edge"> +<title>github.com/containerd/containerd/platforms->strconv</title> +<path fill="none" stroke="#000000" d="M4322.2764,-575.3646C4084.3726,-562.9447 3559.7974,-534.8274 3118,-506 2683.3605,-477.6397 1579.9996,-456.3733 1167,-318 1019.937,-268.7273 983.5252,-238.8094 873,-130 844.4244,-101.868 816.9959,-64.2004 800.8204,-40.516"/> +<polygon fill="#000000" stroke="#000000" points="802.1891,-39.4163 797.9333,-36.2617 799.2931,-41.3817 802.1891,-39.4163"/> +</g> +<!-- github.com/containerd/containerd/platforms->github.com/containerd/containerd/errdefs --> +<g id="edge49" class="edge"> +<title>github.com/containerd/containerd/platforms->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M4322.3319,-571.8957C4091.4136,-553.7618 3609.4274,-515.9116 3380.2642,-497.9155"/> +<polygon fill="#000000" stroke="#000000" points="3380.355,-496.1673 3375.2333,-497.5204 3380.0809,-499.6566 3380.355,-496.1673"/> +</g> +<!-- github.com/containerd/containerd/platforms->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge51" class="edge"> +<title>github.com/containerd/containerd/platforms->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M4322.3532,-578.672C3944.6882,-568.8056 2806.3201,-538.3755 1862,-506 1760.6033,-502.5237 1646.6299,-498.0572 1557.6208,-494.4432"/> +<polygon fill="#000000" stroke="#000000" points="1557.6665,-492.6937 1552.5996,-494.2391 1557.5244,-496.1908 1557.6665,-492.6937"/> +</g> +<!-- github.com/containerd/containerd/platforms->github.com/pkg/errors --> +<g id="edge52" class="edge"> +<title>github.com/containerd/containerd/platforms->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M4579.7226,-565.8313C4684.5946,-551.9978 4836.3282,-530.478 4968,-506 5118.9977,-477.9293 5293.8146,-436.4098 5390.6666,-412.6008"/> +<polygon fill="#000000" stroke="#000000" points="5391.353,-414.2341 5395.7899,-411.3399 5390.5166,-410.8355 5391.353,-414.2341"/> +</g> +<!-- github.com/containerd/containerd/platforms->strings --> +<g id="edge57" class="edge"> +<title>github.com/containerd/containerd/platforms->strings</title> +<path fill="none" stroke="#000000" d="M4579.7388,-572.9867C4913.7607,-548.9063 5802.0793,-480.2494 6089,-412 6299.071,-362.0307 6357.6962,-344.32 6537,-224 6588.1908,-189.6489 6614.2516,-186.4624 6639,-130 6651.8968,-100.5766 6638.6278,-63.7835 6627.039,-40.5786"/> +<polygon fill="#000000" stroke="#000000" points="6628.5928,-39.7734 6624.7519,-36.125 6625.4794,-41.3723 6628.5928,-39.7734"/> +</g> +<!-- github.com/containerd/containerd/platforms->regexp --> +<g id="edge54" class="edge"> +<title>github.com/containerd/containerd/platforms->regexp</title> +<path fill="none" stroke="#000000" d="M4492.4289,-563.901C4605.1146,-515.0116 4927.156,-377.548 5202,-282 5287.4053,-252.3093 5311.1874,-252.492 5397,-224 5402.5773,-222.1482 5408.4729,-220.0797 5414.1678,-218.023"/> +<polygon fill="#000000" stroke="#000000" points="5414.793,-219.6578 5418.8929,-216.3032 5413.5959,-216.3689 5414.793,-219.6578"/> +</g> +<!-- github.com/containerd/containerd/platforms->runtime --> +<g id="edge55" class="edge"> +<title>github.com/containerd/containerd/platforms->runtime</title> +<path fill="none" stroke="#000000" d="M4579.5055,-576.8422C4788.1966,-567.6683 5213.5603,-545.6503 5572,-506 5945.212,-464.7156 6912.9149,-411.9129 7238,-224 7296.5341,-190.1648 7358.9214,-147.0439 7317,-94 7281.6896,-49.3212 7107.1387,-28.1276 7031.7624,-20.9457"/> +<polygon fill="#000000" stroke="#000000" points="7031.6517,-19.1777 7026.51,-20.4527 7031.3245,-22.6624 7031.6517,-19.1777"/> +</g> +<!-- golang.org/x/sync/errgroup->context --> +<g id="edge207" class="edge"> +<title>golang.org/x/sync/errgroup->context</title> +<path fill="none" stroke="#000000" d="M4789.8255,-486.0063C4492.5335,-478.5388 3473.5058,-450.6612 3150,-412 3042.2917,-399.1281 2765.2433,-371.7118 2671,-318 2603.7237,-279.6574 2609.0661,-243.4356 2555,-188 2536.6303,-169.165 2514.8336,-148.8191 2498.4805,-133.9228"/> +<polygon fill="#000000" stroke="#000000" points="2499.3233,-132.3241 2494.4456,-130.2573 2496.9698,-134.9147 2499.3233,-132.3241"/> +</g> +<!-- golang.org/x/sync/errgroup->sync --> +<g id="edge208" class="edge"> +<title>golang.org/x/sync/errgroup->sync</title> +<path fill="none" stroke="#000000" d="M4908.0634,-469.9668C4983.5093,-432.5652 5163.9482,-344.8058 5320,-282 5392.8893,-252.6644 5418.3373,-262.195 5487,-224 5544.9895,-191.7421 5574.4424,-189.8984 5603,-130 5616.6941,-101.2772 5604.5527,-64.5209 5593.599,-41.1103"/> +<polygon fill="#000000" stroke="#000000" points="5595.0388,-40.0673 5591.2893,-36.325 5591.8867,-41.5887 5595.0388,-40.0673"/> +</g> +<!-- golang.org/x/sync/semaphore->container/list --> +<g id="edge209" class="edge"> +<title>golang.org/x/sync/semaphore->container/list</title> +<path fill="none" stroke="#000000" d="M4704.4765,-187.9871C4660.7806,-171.4249 4596.2979,-146.9839 4552.344,-130.3239"/> +<polygon fill="#000000" stroke="#000000" points="4552.9188,-128.6704 4547.6231,-128.5346 4551.6783,-131.9432 4552.9188,-128.6704"/> +</g> +<!-- golang.org/x/sync/semaphore->context --> +<g id="edge210" class="edge"> +<title>golang.org/x/sync/semaphore->context</title> +<path fill="none" stroke="#000000" d="M4663.4131,-204.23C4377.3008,-198.1938 3452.166,-176.181 2688,-130 2623.9371,-126.1285 2549.2699,-119.325 2507.2246,-115.283"/> +<polygon fill="#000000" stroke="#000000" points="2507.1572,-113.5184 2502.0122,-114.7799 2506.821,-117.0022 2507.1572,-113.5184"/> +</g> +<!-- golang.org/x/sync/semaphore->sync --> +<g id="edge211" class="edge"> +<title>golang.org/x/sync/semaphore->sync</title> +<path fill="none" stroke="#000000" d="M4809.5083,-187.9853C4886.811,-164.2111 5028.7802,-122.0947 5152,-94 5298.9164,-60.5023 5476.9718,-33.1153 5548.7301,-22.6142"/> +<polygon fill="#000000" stroke="#000000" points="5549.0162,-24.3411 5553.7111,-21.8875 5548.5108,-20.8778 5549.0162,-24.3411"/> +</g> +<!-- github.com/containerd/containerd/labels --> +<g id="node35" class="node"> +<title>github.com/containerd/containerd/labels</title> +<g id="a_node35"><a xlink:href="https://godoc.org/github.com/containerd/containerd/labels" xlink:title="github.com/containerd/containerd/labels" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5620,-600C5620,-600 5408,-600 5408,-600 5402,-600 5396,-594 5396,-588 5396,-588 5396,-576 5396,-576 5396,-570 5402,-564 5408,-564 5408,-564 5620,-564 5620,-564 5626,-564 5632,-570 5632,-576 5632,-576 5632,-588 5632,-588 5632,-594 5626,-600 5620,-600"/> +<text text-anchor="middle" x="5514" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/labels</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/labels->github.com/containerd/containerd/errdefs --> +<g id="edge43" class="edge"> +<title>github.com/containerd/containerd/labels->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M5395.6746,-577.0785C5008.2098,-560.9627 3779.517,-509.8578 3380.4896,-493.2611"/> +<polygon fill="#000000" stroke="#000000" points="3380.3228,-491.5027 3375.2544,-493.0433 3380.1773,-494.9997 3380.3228,-491.5027"/> +</g> +<!-- github.com/containerd/containerd/labels->github.com/pkg/errors --> +<g id="edge44" class="edge"> +<title>github.com/containerd/containerd/labels->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M5528.2881,-563.7154C5544.048,-541.1716 5565.3913,-501.9235 5551,-470 5540.6463,-447.0329 5519.5416,-428.2136 5500.8313,-415.0474"/> +<polygon fill="#000000" stroke="#000000" points="5501.7438,-413.551 5496.6329,-412.1577 5499.7594,-416.4341 5501.7438,-413.551"/> +</g> +<!-- github.com/sirupsen/logrus->bufio --> +<g id="edge186" class="edge"> +<title>github.com/sirupsen/logrus->bufio</title> +<path fill="none" stroke="#000000" d="M4950.2956,-187.8823C4902.7559,-173.4625 4835.6307,-152.0967 4778,-130 4697.3969,-99.0952 4605.3245,-56.164 4557.9243,-33.4786"/> +<polygon fill="#000000" stroke="#000000" points="4558.4662,-31.7977 4553.201,-31.2143 4556.9531,-34.9538 4558.4662,-31.7977"/> +</g> +<!-- github.com/sirupsen/logrus->bytes --> +<g id="edge187" class="edge"> +<title>github.com/sirupsen/logrus->bytes</title> +<path fill="none" stroke="#000000" d="M5008.5482,-187.9738C5002.0064,-153.8113 4987.8075,-79.6613 4980.5179,-41.5936"/> +<polygon fill="#000000" stroke="#000000" points="4982.1535,-40.8295 4979.4943,-36.2479 4978.7159,-41.4878 4982.1535,-40.8295"/> +</g> +<!-- github.com/sirupsen/logrus->context --> +<g id="edge188" class="edge"> +<title>github.com/sirupsen/logrus->context</title> +<path fill="none" stroke="#000000" d="M4929.4228,-195.1615C4905.4575,-192.3879 4879.2202,-189.7093 4855,-188 3893.9342,-120.1759 3649.9813,-183.2872 2688,-130 2623.9185,-126.4503 2549.2577,-119.5358 2507.2186,-115.3866"/> +<polygon fill="#000000" stroke="#000000" points="2507.1553,-113.6218 2502.007,-114.8698 2506.8099,-117.1047 2507.1553,-113.6218"/> +</g> +<!-- github.com/sirupsen/logrus->encoding/json --> +<g id="edge189" class="edge"> +<title>github.com/sirupsen/logrus->encoding/json</title> +<path fill="none" stroke="#000000" d="M5094.681,-190.048C5167.6634,-175.5983 5276.7939,-153.092 5371,-130 5428.8125,-115.8289 5442.3402,-108.7802 5500,-94 5596.419,-69.2844 5709.771,-43.5191 5773.9208,-29.2405"/> +<polygon fill="#000000" stroke="#000000" points="5774.3323,-30.9419 5778.8332,-28.1482 5773.5725,-27.5253 5774.3323,-30.9419"/> +</g> +<!-- github.com/sirupsen/logrus->fmt --> +<g id="edge190" class="edge"> +<title>github.com/sirupsen/logrus->fmt</title> +<path fill="none" stroke="#000000" d="M4929.3841,-195.6542C4905.4181,-192.89 4879.19,-190.0929 4855,-188 4415.444,-149.9694 4303.5562,-168.0278 3864,-130 3491.9236,-97.8101 3042.3637,-38.8556 2919.4065,-22.3797"/> +<polygon fill="#000000" stroke="#000000" points="2919.329,-20.6037 2914.1407,-21.6733 2918.8636,-24.0726 2919.329,-20.6037"/> +</g> +<!-- github.com/sirupsen/logrus->io --> +<g id="edge192" class="edge"> +<title>github.com/sirupsen/logrus->io</title> +<path fill="none" stroke="#000000" d="M4929.4215,-195.1807C4905.4562,-192.4076 4879.2191,-189.7243 4855,-188 3936.5195,-122.609 3701.8039,-204.2869 2784,-130 2664.2108,-120.3043 2635.7227,-104.4844 2516,-94 1892.0793,-39.3618 1729.6939,-111.8883 1108,-36 1089.3427,-33.7226 1068.7466,-29.5995 1052.1731,-25.8736"/> +<polygon fill="#000000" stroke="#000000" points="1052.4293,-24.1372 1047.1654,-24.7325 1051.6516,-27.5497 1052.4293,-24.1372"/> +</g> +<!-- github.com/sirupsen/logrus->os --> +<g id="edge194" class="edge"> +<title>github.com/sirupsen/logrus->os</title> +<path fill="none" stroke="#000000" d="M4929.3775,-195.7294C4905.4114,-192.9666 4879.1849,-190.1515 4855,-188 4652.06,-169.9463 4133.0332,-195.1825 3940,-130 3882.7878,-110.6809 3826.5009,-66.647 3795.8004,-39.9081"/> +<polygon fill="#000000" stroke="#000000" points="3796.734,-38.399 3791.8222,-36.4149 3794.4246,-41.029 3796.734,-38.399"/> +</g> +<!-- github.com/sirupsen/logrus->sync --> +<g id="edge199" class="edge"> +<title>github.com/sirupsen/logrus->sync</title> +<path fill="none" stroke="#000000" d="M5094.7172,-191.0166C5156.6085,-178.4342 5242.6774,-158.0016 5315,-130 5346.5238,-117.7947 5351.2582,-108.0591 5382,-94 5439.4539,-67.7246 5508.8431,-42.6741 5548.6547,-28.9141"/> +<polygon fill="#000000" stroke="#000000" points="5549.677,-30.4129 5553.8345,-27.1299 5548.5371,-27.1038 5549.677,-30.4129"/> +</g> +<!-- github.com/sirupsen/logrus->time --> +<g id="edge201" class="edge"> +<title>github.com/sirupsen/logrus->time</title> +<path fill="none" stroke="#000000" d="M4929.4039,-195.4145C4905.4383,-192.6457 4879.2055,-189.9063 4855,-188 4257.6283,-140.9547 4106.5709,-157.9134 3508,-130 3189.2783,-115.1369 3109.6972,-109.3807 2791,-94 2142.8381,-62.719 1350.5377,-27.0067 1182.3252,-19.4502"/> +<polygon fill="#000000" stroke="#000000" points="1182.1407,-17.6903 1177.0671,-19.2141 1181.9836,-21.1868 1182.1407,-17.6903"/> +</g> +<!-- github.com/sirupsen/logrus->strings --> +<g id="edge198" class="edge"> +<title>github.com/sirupsen/logrus->strings</title> +<path fill="none" stroke="#000000" d="M5094.7534,-191.3629C5102.2669,-190.1703 5109.7674,-189.0298 5117,-188 5702.9958,-104.5673 6422.7257,-35.802 6581.8753,-20.9659"/> +<polygon fill="#000000" stroke="#000000" points="6582.0444,-22.7078 6586.8606,-20.5017 6581.7199,-19.2229 6582.0444,-22.7078"/> +</g> +<!-- github.com/sirupsen/logrus->sort --> +<g id="edge197" class="edge"> +<title>github.com/sirupsen/logrus->sort</title> +<path fill="none" stroke="#000000" d="M5094.5358,-191.8406C5102.1305,-190.5408 5109.7075,-189.2453 5117,-188 5268.5387,-162.1235 5306.5345,-156.3019 5458,-130 5688.0499,-90.052 5964.9174,-40.4643 6057.8372,-23.7808"/> +<polygon fill="#000000" stroke="#000000" points="6058.2965,-25.4764 6062.9085,-22.8701 6057.6779,-22.0315 6058.2965,-25.4764"/> +</g> +<!-- github.com/sirupsen/logrus->sync/atomic --> +<g id="edge200" class="edge"> +<title>github.com/sirupsen/logrus->sync/atomic</title> +<path fill="none" stroke="#000000" d="M4929.3492,-193.0786C4887.4079,-182.5813 4838.5801,-163.7196 4806,-130 4782.6197,-105.802 4774.0097,-66.5266 4770.8415,-41.4972"/> +<polygon fill="#000000" stroke="#000000" points="4772.5625,-41.1464 4770.2446,-36.383 4769.0861,-41.5522 4772.5625,-41.1464"/> +</g> +<!-- github.com/sirupsen/logrus->runtime --> +<g id="edge196" class="edge"> +<title>github.com/sirupsen/logrus->runtime</title> +<path fill="none" stroke="#000000" d="M5094.9792,-202.1258C5375.3404,-188.9464 6277.1331,-145.7927 6407,-130 6620.0577,-104.0908 6871.9738,-47.4279 6962.484,-26.2251"/> +<polygon fill="#000000" stroke="#000000" points="6962.9753,-27.9074 6967.443,-25.061 6962.1754,-24.5001 6962.9753,-27.9074"/> +</g> +<!-- log --> +<g id="node52" class="node"> +<title>log</title> +<g id="a_node52"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3935,-36C3935,-36 3905,-36 3905,-36 3899,-36 3893,-30 3893,-24 3893,-24 3893,-12 3893,-12 3893,-6 3899,0 3905,0 3905,0 3935,0 3935,0 3941,0 3947,-6 3947,-12 3947,-12 3947,-24 3947,-24 3947,-30 3941,-36 3935,-36"/> +<text text-anchor="middle" x="3920" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->log --> +<g id="edge193" class="edge"> +<title>github.com/sirupsen/logrus->log</title> +<path fill="none" stroke="#000000" d="M4929.3737,-195.7719C4905.4075,-193.0099 4879.1819,-190.1846 4855,-188 4757.7388,-179.2135 4059.7133,-183.4778 3978,-130 3947.0816,-109.7652 3931.7227,-67.5412 3924.8449,-41.11"/> +<polygon fill="#000000" stroke="#000000" points="3926.4896,-40.4726 3923.5803,-36.0455 3923.0938,-41.3205 3926.4896,-40.4726"/> +</g> +<!-- reflect --> +<g id="node54" class="node"> +<title>reflect</title> +<g id="a_node54"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4228,-36C4228,-36 4198,-36 4198,-36 4192,-36 4186,-30 4186,-24 4186,-24 4186,-12 4186,-12 4186,-6 4192,0 4198,0 4198,0 4228,0 4228,0 4234,0 4240,-6 4240,-12 4240,-12 4240,-24 4240,-24 4240,-30 4234,-36 4228,-36"/> +<text text-anchor="middle" x="4213" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->reflect --> +<g id="edge195" class="edge"> +<title>github.com/sirupsen/logrus->reflect</title> +<path fill="none" stroke="#000000" d="M4929.5706,-187.9369C4863.7053,-173.212 4769.1441,-151.4186 4687,-130 4631.0568,-115.4131 4618.1579,-107.7373 4562,-94 4435.5929,-63.0784 4401.3739,-67.0571 4275,-36 4265.1751,-33.5855 4254.5704,-30.6358 4244.9574,-27.8285"/> +<polygon fill="#000000" stroke="#000000" points="4245.3209,-26.1113 4240.0302,-26.3771 4244.3319,-29.4687 4245.3209,-26.1113"/> +</g> +<!-- golang.org/x/sys/unix --> +<g id="node62" class="node"> +<title>golang.org/x/sys/unix</title> +<g id="a_node62"><a xlink:href="https://godoc.org/golang.org/x/sys/unix" xlink:title="golang.org/x/sys/unix" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5289,-130C5289,-130 5179,-130 5179,-130 5173,-130 5167,-124 5167,-118 5167,-118 5167,-106 5167,-106 5167,-100 5173,-94 5179,-94 5179,-94 5289,-94 5289,-94 5295,-94 5301,-100 5301,-106 5301,-106 5301,-118 5301,-118 5301,-124 5295,-130 5289,-130"/> +<text text-anchor="middle" x="5234" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sys/unix</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->golang.org/x/sys/unix --> +<g id="edge191" class="edge"> +<title>github.com/sirupsen/logrus->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M5054.5412,-187.9871C5092.2879,-172.0042 5147.3632,-148.6841 5186.5296,-132.1001"/> +<polygon fill="#000000" stroke="#000000" points="5187.303,-133.6731 5191.2249,-130.112 5185.9383,-130.4501 5187.303,-133.6731"/> +</g> +<!-- github.com/containerd/containerd/reference --> +<g id="node40" class="node"> +<title>github.com/containerd/containerd/reference</title> +<g id="a_node40"><a xlink:href="https://godoc.org/github.com/containerd/containerd/reference" xlink:title="github.com/containerd/containerd/reference" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5982,-412C5982,-412 5752,-412 5752,-412 5746,-412 5740,-406 5740,-400 5740,-400 5740,-388 5740,-388 5740,-382 5746,-376 5752,-376 5752,-376 5982,-376 5982,-376 5988,-376 5994,-382 5994,-388 5994,-388 5994,-400 5994,-400 5994,-406 5988,-412 5982,-412"/> +<text text-anchor="middle" x="5867" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/reference</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/reference->errors --> +<g id="edge58" class="edge"> +<title>github.com/containerd/containerd/reference->errors</title> +<path fill="none" stroke="#000000" d="M5841.2979,-375.8358C5782.0108,-334.9163 5629.7974,-235.4049 5487,-188 5472.97,-183.3424 4535.9512,-49.291 4349.1765,-22.5971"/> +<polygon fill="#000000" stroke="#000000" points="4349.3255,-20.8507 4344.1282,-21.8756 4348.8303,-24.3155 4349.3255,-20.8507"/> +</g> +<!-- github.com/containerd/containerd/reference->fmt --> +<g id="edge59" class="edge"> +<title>github.com/containerd/containerd/reference->fmt</title> +<path fill="none" stroke="#000000" d="M5739.7534,-385.9888C5681.3487,-382.5254 5611.1797,-378.6645 5548,-376 4625.7931,-337.107 4388.1865,-430.1635 3472,-318 3278.2564,-294.2811 3209.7168,-325.512 3043,-224 2968.8745,-178.8658 2917.6962,-84.3114 2897.1123,-40.8006"/> +<polygon fill="#000000" stroke="#000000" points="2898.6885,-40.0397 2894.9845,-36.2525 2895.5183,-41.523 2898.6885,-40.0397"/> +</g> +<!-- github.com/containerd/containerd/reference->github.com/opencontainers/go-digest --> +<g id="edge60" class="edge"> +<title>github.com/containerd/containerd/reference->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M5739.7607,-385.8088C5681.3575,-382.3092 5611.1876,-378.4696 5548,-376 5204.848,-362.5884 2734.7241,-313.4034 2170.5403,-302.2566"/> +<polygon fill="#000000" stroke="#000000" points="2170.243,-300.5004 2165.2094,-302.1512 2170.1738,-303.9998 2170.243,-300.5004"/> +</g> +<!-- github.com/containerd/containerd/reference->strings --> +<g id="edge64" class="edge"> +<title>github.com/containerd/containerd/reference->strings</title> +<path fill="none" stroke="#000000" d="M5938.2336,-375.9356C6099.4178,-333.7744 6485.2279,-225.2244 6577,-130 6600.3494,-105.7722 6608.9681,-66.5052 6612.1467,-41.4858"/> +<polygon fill="#000000" stroke="#000000" points="6613.9018,-41.5434 6612.7457,-36.3737 6610.4256,-41.136 6613.9018,-41.5434"/> +</g> +<!-- github.com/containerd/containerd/reference->regexp --> +<g id="edge63" class="edge"> +<title>github.com/containerd/containerd/reference->regexp</title> +<path fill="none" stroke="#000000" d="M5785.9056,-375.9744C5735.3849,-363.1943 5669.8041,-343.7397 5615,-318 5561.1841,-292.7243 5504.8086,-252.0421 5472.6813,-227.289"/> +<polygon fill="#000000" stroke="#000000" points="5473.528,-225.7313 5468.503,-224.054 5471.3853,-228.4988 5473.528,-225.7313"/> +</g> +<!-- net/url --> +<g id="node41" class="node"> +<title>net/url</title> +<g id="a_node41"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7290,-130C7290,-130 7260,-130 7260,-130 7254,-130 7248,-124 7248,-118 7248,-118 7248,-106 7248,-106 7248,-100 7254,-94 7260,-94 7260,-94 7290,-94 7290,-94 7296,-94 7302,-100 7302,-106 7302,-106 7302,-118 7302,-118 7302,-124 7296,-130 7290,-130"/> +<text text-anchor="middle" x="7275" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/reference->net/url --> +<g id="edge61" class="edge"> +<title>github.com/containerd/containerd/reference->net/url</title> +<path fill="none" stroke="#000000" d="M5994.0069,-383.7068C6306.7824,-357.5239 7096.2659,-286.0623 7198,-224 7231.9449,-203.2921 7254.5649,-161.3174 7266.0611,-135.0462"/> +<polygon fill="#000000" stroke="#000000" points="7267.7201,-135.6175 7268.0785,-130.3322 7264.5024,-134.2404 7267.7201,-135.6175"/> +</g> +<!-- github.com/containerd/containerd/reference->path --> +<g id="edge62" class="edge"> +<title>github.com/containerd/containerd/reference->path</title> +<path fill="none" stroke="#000000" d="M5896.6927,-375.8759C5923.2976,-359.6366 5962.2044,-335.8882 5989.3825,-319.299"/> +<polygon fill="#000000" stroke="#000000" points="5990.6209,-320.5934 5993.977,-316.4946 5988.7974,-317.6059 5990.6209,-320.5934"/> +</g> +<!-- github.com/containerd/containerd/remotes --> +<g id="node43" class="node"> +<title>github.com/containerd/containerd/remotes</title> +<g id="a_node43"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes" xlink:title="github.com/containerd/containerd/remotes" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3725,-788C3725,-788 3501,-788 3501,-788 3495,-788 3489,-782 3489,-776 3489,-776 3489,-764 3489,-764 3489,-758 3495,-752 3501,-752 3501,-752 3725,-752 3725,-752 3731,-752 3737,-758 3737,-764 3737,-764 3737,-776 3737,-776 3737,-782 3731,-788 3725,-788"/> +<text text-anchor="middle" x="3613" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes->context --> +<g id="edge65" class="edge"> +<title>github.com/containerd/containerd/remotes->context</title> +<path fill="none" stroke="#000000" d="M3488.7927,-757.9452C3233.5893,-731.4978 2674,-664.1409 2674,-582 2674,-582 2674,-582 2674,-488 2674,-436.4674 2677.6692,-415.0127 2644,-376 2601.6004,-326.8713 2554.4569,-365.2752 2510,-318 2462.5354,-267.5264 2465.8312,-177.8284 2470.5842,-135.4353"/> +<polygon fill="#000000" stroke="#000000" points="2472.3466,-135.4336 2471.1982,-130.2622 2468.871,-135.021 2472.3466,-135.4336"/> +</g> +<!-- github.com/containerd/containerd/remotes->fmt --> +<g id="edge66" class="edge"> +<title>github.com/containerd/containerd/remotes->fmt</title> +<path fill="none" stroke="#000000" d="M3488.625,-763.9719C3298.7184,-753.7603 2945.7825,-730.5358 2825,-694 2570.7311,-617.0855 2434.5175,-639.8848 2298,-412 2219.2715,-280.5806 2305.9019,-182.4291 2431,-94 2499.8245,-45.3495 2762.6518,-25.3717 2854.8259,-19.7656"/> +<polygon fill="#000000" stroke="#000000" points="2854.9821,-21.5095 2859.8682,-19.463 2854.7723,-18.0158 2854.9821,-21.5095"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/containerd/containerd/log --> +<g id="edge70" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M3737.3654,-758.5467C3974.1272,-735.2487 4479.9371,-677.8745 4636,-600 4645.0245,-595.4968 4858.4388,-389.5064 4928.7577,-321.5187"/> +<polygon fill="#000000" stroke="#000000" points="4930.0148,-322.7374 4932.3928,-318.0037 4927.5818,-320.2213 4930.0148,-322.7374"/> +</g> +<!-- github.com/containerd/containerd/remotes->io --> +<g id="edge75" class="edge"> +<title>github.com/containerd/containerd/remotes->io</title> +<path fill="none" stroke="#000000" d="M3488.6993,-765.4998C2993.2005,-747.1027 1173.7861,-675.0242 927,-600 676.8598,-523.9561 460.6046,-295.6565 627,-94 650.119,-65.9818 897.6287,-33.034 987.5704,-21.8989"/> +<polygon fill="#000000" stroke="#000000" points="988.0066,-23.6084 992.7548,-21.2596 987.5782,-20.1347 988.0066,-23.6084"/> +</g> +<!-- github.com/containerd/containerd/remotes->sync --> +<g id="edge77" class="edge"> +<title>github.com/containerd/containerd/remotes->sync</title> +<path fill="none" stroke="#000000" d="M3737.091,-766.9878C3903.8116,-761.111 4208.6992,-743.92 4464,-694 4977.7539,-593.5436 5132.3449,-547.3801 5544,-224 5588.2929,-189.2052 5608.759,-182.5628 5629,-130 5641.1665,-98.4055 5619.362,-62.5424 5601.3058,-40.0954"/> +<polygon fill="#000000" stroke="#000000" points="5602.516,-38.812 5597.9869,-36.0642 5599.8139,-41.0366 5602.516,-38.812"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/containerd/containerd/content --> +<g id="edge67" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/containerd/containerd/content</title> +<path fill="none" stroke="#000000" d="M3488.9905,-766.637C3288.1743,-759.9821 2879.7171,-741.4268 2537,-694 2370.1199,-670.9063 2177.5341,-626.6471 2073.4178,-601.2278"/> +<polygon fill="#000000" stroke="#000000" points="2073.7998,-599.5197 2068.5272,-600.0318 2072.9683,-602.9195 2073.7998,-599.5197"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/containerd/containerd/errdefs --> +<g id="edge68" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M3572.218,-751.8387C3514.7297,-724.8126 3408.7669,-669.6355 3336,-600 3307.4801,-572.7073 3281.8851,-534.4863 3267.1486,-510.4965"/> +<polygon fill="#000000" stroke="#000000" points="3268.6204,-509.5487 3264.5251,-506.1885 3265.6311,-511.3692 3268.6204,-509.5487"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge72" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M3488.5587,-765.9854C3259.8601,-757.8536 2757.5078,-736.4871 2336,-694 2041.2003,-664.2848 1961.92,-677.7149 1676,-600 1590.5823,-576.7829 1496.1043,-533.885 1443.7081,-508.4495"/> +<polygon fill="#000000" stroke="#000000" points="1444.3419,-506.8117 1439.0804,-506.1952 1442.8091,-509.9582 1444.3419,-506.8117"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/pkg/errors --> +<g id="edge73" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M3737.044,-768.1019C4024.6262,-762.8781 4729.9151,-745.113 4961,-694 5071.2384,-669.6167 5098.8634,-655.8236 5197,-600 5295.0397,-544.2315 5396.9303,-456.0869 5441.6276,-415.6157"/> +<polygon fill="#000000" stroke="#000000" points="5442.9917,-416.7408 5445.5166,-412.0838 5440.6387,-414.1498 5442.9917,-416.7408"/> +</g> +<!-- github.com/containerd/containerd/remotes->strings --> +<g id="edge76" class="edge"> +<title>github.com/containerd/containerd/remotes->strings</title> +<path fill="none" stroke="#000000" d="M3737.2736,-766.7935C3986.0064,-759.6275 4562.5779,-739.2573 5045,-694 5740.3951,-628.7632 5953.5604,-697.4836 6591,-412 6763.1318,-334.9091 6984.9617,-245.0364 6872,-94 6844.73,-57.5384 6709.2853,-32.5244 6646.3035,-22.6762"/> +<polygon fill="#000000" stroke="#000000" points="6646.3764,-20.9167 6641.1676,-21.8817 6645.8412,-24.3756 6646.3764,-20.9167"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/containerd/containerd/images --> +<g id="edge69" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/containerd/containerd/images</title> +<path fill="none" stroke="#000000" d="M3613,-751.8759C3613,-736.9211 3613,-715.5983 3613,-699.3629"/> +<polygon fill="#000000" stroke="#000000" points="3614.7501,-699.0685 3613,-694.0685 3611.2501,-699.0685 3614.7501,-699.0685"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/containerd/containerd/platforms --> +<g id="edge71" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/containerd/containerd/platforms</title> +<path fill="none" stroke="#000000" d="M3693.351,-751.9738C3851.7051,-716.448 4202.8112,-637.6796 4365.3572,-601.2134"/> +<polygon fill="#000000" stroke="#000000" points="4366.0896,-602.8427 4370.5852,-600.0406 4365.3234,-599.4276 4366.0896,-602.8427"/> +</g> +<!-- github.com/containerd/containerd/remotes->github.com/sirupsen/logrus --> +<g id="edge74" class="edge"> +<title>github.com/containerd/containerd/remotes->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M3737.0407,-765.2426C3888.6146,-757.6835 4152.2804,-738.8783 4374,-694 4512.6118,-665.9436 4543.1754,-642.6933 4678,-600 4807.1693,-559.0975 4849.6229,-571.9135 4968,-506 5024.481,-474.5508 5051.812,-470.6513 5079,-412 5087.4344,-393.805 5093.5239,-327.7361 5076,-282 5067.9893,-261.0926 5051.6494,-241.8489 5037.4857,-227.9565"/> +<polygon fill="#000000" stroke="#000000" points="5038.3755,-226.3847 5033.5572,-224.1831 5035.9509,-228.9089 5038.3755,-226.3847"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker --> +<g id="node44" class="node"> +<title>github.com/containerd/containerd/remotes/docker</title> +<g id="a_node44"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes/docker" xlink:title="github.com/containerd/containerd/remotes/docker" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5989,-976C5989,-976 5725,-976 5725,-976 5719,-976 5713,-970 5713,-964 5713,-964 5713,-952 5713,-952 5713,-946 5719,-940 5725,-940 5725,-940 5989,-940 5989,-940 5995,-940 6001,-946 6001,-952 6001,-952 6001,-964 6001,-964 6001,-970 5995,-976 5989,-976"/> +<text text-anchor="middle" x="5857" y="-954.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes/docker</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->bytes --> +<g id="edge78" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->bytes</title> +<path fill="none" stroke="#000000" d="M5893.8804,-939.8635C5917.2107,-926.7 5946.3763,-906.8651 5965,-882 6049.7537,-768.8427 6050.0175,-682.5582 5973,-564 5936.2835,-507.4799 5911.6156,-504.9723 5854,-470 5800.263,-437.3819 5775.5243,-449.4019 5725,-412 5618.6768,-333.2915 5644.9454,-251.6892 5529,-188 5380.4141,-106.3814 5310.5403,-190.0361 5152,-130 5124.3294,-119.5217 5120.9429,-109.9157 5096,-94 5066.0572,-74.8939 5031.8209,-53.2288 5007.6013,-37.9325"/> +<polygon fill="#000000" stroke="#000000" points="5008.3609,-36.3425 5003.1988,-35.1525 5006.4921,-39.3019 5008.3609,-36.3425"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->context --> +<g id="edge79" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->context</title> +<path fill="none" stroke="#000000" d="M5712.7449,-956.193C5121.0696,-948.6011 2878.1511,-918.0075 2170,-882 1912.1533,-868.8892 1187.0196,-977.8024 1012,-788 931.5387,-700.7426 342.8116,-1264.2126 1092,-376 1273.3864,-160.9544 2247.2244,-119.1609 2440.6036,-112.9436"/> +<polygon fill="#000000" stroke="#000000" points="2440.8874,-114.6856 2445.8295,-112.7782 2440.7766,-111.1874 2440.8874,-114.6856"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->encoding/base64 --> +<g id="edge80" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->encoding/base64</title> +<path fill="none" stroke="#000000" d="M5811.2828,-939.912C5782.0918,-926.8279 5744.6724,-907.0636 5717,-882 5688.4259,-856.1196 5665.2621,-817.4112 5652.3098,-792.9464"/> +<polygon fill="#000000" stroke="#000000" points="5653.7233,-791.8727 5649.8569,-788.2514 5650.6211,-793.4934 5653.7233,-791.8727"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->encoding/json --> +<g id="edge81" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->encoding/json</title> +<path fill="none" stroke="#000000" d="M6001.0276,-949.9455C6141.861,-940.0217 6344.903,-919.6482 6414,-882 6471.5703,-850.6322 6510,-835.5612 6510,-770 6510,-770 6510,-770 6510,-488 6510,-240.3471 6310.8683,-226.9948 6083,-130 6008.5721,-98.3189 5922.4352,-60.7412 5870.9084,-38.1641"/> +<polygon fill="#000000" stroke="#000000" points="5871.3135,-36.431 5866.0315,-36.0267 5869.9085,-39.6367 5871.3135,-36.431"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->fmt --> +<g id="edge82" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->fmt</title> +<path fill="none" stroke="#000000" d="M5712.8551,-957.8407C4880.5707,-956.686 722.5806,-947.636 609,-882 506.1407,-822.5596 472,-565.1488 472,-488 472,-488 472,-488 472,-394 472,-286.0484 806.213,-154.2271 1062,-94 1242.9819,-51.3864 2624.0586,-23.0658 2854.4576,-18.6149"/> +<polygon fill="#000000" stroke="#000000" points="2854.7196,-20.3603 2859.6849,-18.5142 2854.6521,-16.8609 2854.7196,-20.3603"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/log --> +<g id="edge87" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M5852.0515,-939.9706C5840.3822,-900.6129 5807.5481,-806.1972 5748,-752 5617.4339,-633.1662 5535.8577,-684.7826 5381,-600 5218.4948,-511.0304 5042.5863,-373.9018 4977.3057,-321.4285"/> +<polygon fill="#000000" stroke="#000000" points="4978.1591,-319.8688 4973.1671,-318.096 4975.964,-322.5949 4978.1591,-319.8688"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->io --> +<g id="edge98" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->io</title> +<path fill="none" stroke="#000000" d="M5712.6806,-957.8779C4874.1611,-956.932 657.0564,-948.8558 392,-882 287.5284,-855.6489 177,-877.7436 177,-770 177,-770 177,-770 177,-582 177,-453.9529 236,-428.0471 236,-300 236,-300 236,-300 236,-206 236,-133.0461 289.762,-124.5744 356,-94 385.0945,-80.5705 858.5389,-33.714 987.6817,-21.1307"/> +<polygon fill="#000000" stroke="#000000" points="988.0446,-22.8538 992.8515,-20.6274 987.7054,-19.3702 988.0446,-22.8538"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->sync --> +<g id="edge105" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->sync</title> +<path fill="none" stroke="#000000" d="M6001.0118,-941.6052C6104.2416,-928.0996 6233.1649,-907.1241 6281,-882 6343.0455,-849.4123 6392,-840.0828 6392,-770 6392,-770 6392,-770 6392,-488 6392,-180.4017 6106.3403,-213.7288 5823,-94 5750.0075,-63.1562 5660.0626,-38.1232 5613.0607,-25.9713"/> +<polygon fill="#000000" stroke="#000000" points="5613.3752,-24.2453 5608.0968,-24.6946 5612.5033,-27.635 5613.3752,-24.2453"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/content --> +<g id="edge83" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/content</title> +<path fill="none" stroke="#000000" d="M5712.7483,-957.4584C5061.6795,-954.6949 2421.4798,-939.9455 2256,-882 2137.7688,-840.5993 2114.6068,-798.8129 2046,-694 2027.3349,-665.4846 2012.8063,-628.6755 2004.5687,-605.1968"/> +<polygon fill="#000000" stroke="#000000" points="2006.1965,-604.5493 2002.9071,-600.3968 2002.889,-605.6942 2006.1965,-604.5493"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/errdefs --> +<g id="edge84" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M5712.6814,-955.2407C5164.4755,-943.8244 3242,-894.8858 3242,-770 3242,-770 3242,-770 3242,-676 3242,-616.705 3248.0187,-547.1037 3251.5311,-511.5337"/> +<polygon fill="#000000" stroke="#000000" points="3253.3158,-511.2733 3252.072,-506.124 3249.8331,-510.925 3253.3158,-511.2733"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/opencontainers/go-digest --> +<g id="edge93" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M5712.6937,-957.1814C4877.3713,-952.2881 697.3237,-925.6344 649,-882 279.3722,-548.2406 1084.0163,-378.1375 1092,-376 1453.4906,-279.2178 1559.8999,-357.7966 1932,-318 1935.1938,-317.6584 1938.4317,-317.2964 1941.698,-316.9173"/> +<polygon fill="#000000" stroke="#000000" points="1942.0918,-318.633 1946.8518,-316.3082 1941.681,-315.1572 1942.0918,-318.633"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge94" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M5712.8296,-957.0129C4881.7833,-951.187 738.0921,-920.238 686,-882 643.7396,-850.979 649,-822.4237 649,-770 649,-770 649,-770 649,-676 649,-553.6886 1027.2628,-510.3263 1247.9489,-495.4358"/> +<polygon fill="#000000" stroke="#000000" points="1248.2602,-497.169 1253.1324,-495.0894 1248.0268,-493.6768 1248.2602,-497.169"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/pkg/errors --> +<g id="edge95" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M5866.7427,-939.916C5883.5003,-907.0719 5916,-834.996 5916,-770 5916,-770 5916,-770 5916,-676 5916,-500.6663 5667.9148,-430.291 5539.7403,-405.6317"/> +<polygon fill="#000000" stroke="#000000" points="5539.7192,-403.8465 5534.4804,-404.6325 5539.0659,-407.285 5539.7192,-403.8465"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->io/ioutil --> +<g id="edge99" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->io/ioutil</title> +<path fill="none" stroke="#000000" d="M5712.7605,-956.988C4844.7911,-950.785 349.2635,-917.0122 212,-882 108.7432,-855.662 0,-876.5629 0,-770 0,-770 0,-770 0,-206 0,-140.9881 43.6996,-74.2442 69.5096,-40.4935"/> +<polygon fill="#000000" stroke="#000000" points="71.1951,-41.175 72.8723,-36.1501 68.4276,-39.0324 71.1951,-41.175"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->time --> +<g id="edge106" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->time</title> +<path fill="none" stroke="#000000" d="M5712.8875,-957.8482C4890.6398,-956.7414 819.9318,-947.9386 564,-882 461.5673,-855.6091 354,-875.7778 354,-770 354,-770 354,-770 354,-582 354,-453.9529 413,-428.0471 413,-300 413,-300 413,-300 413,-206 413,-169.0883 975.3755,-53.2085 1117.6191,-24.4889"/> +<polygon fill="#000000" stroke="#000000" points="1118.3405,-26.1287 1122.8957,-23.4246 1117.6484,-22.6978 1118.3405,-26.1287"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->strings --> +<g id="edge104" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->strings</title> +<path fill="none" stroke="#000000" d="M6001.2152,-948.3295C6192.9633,-934.7212 6521.3367,-908.597 6639,-882 6781.4815,-849.7931 6948,-916.0763 6948,-770 6948,-770 6948,-770 6948,-300 6948,-206.3793 6968.6641,-161.7008 6904,-94 6868.4379,-56.7679 6714.6337,-31.6915 6646.5909,-22.2199"/> +<polygon fill="#000000" stroke="#000000" points="6646.6433,-20.4607 6641.4512,-21.5115 6646.1654,-23.9279 6646.6433,-20.4607"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/images --> +<g id="edge85" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/images</title> +<path fill="none" stroke="#000000" d="M5833.6601,-939.8145C5816.2664,-925.4398 5792.7286,-904.1634 5776,-882 5736.8413,-830.1195 5763.878,-786.8338 5709,-752 5625.6974,-699.1236 4180.5748,-681.3019 3739.6784,-677.0819"/> +<polygon fill="#000000" stroke="#000000" points="3739.5543,-675.3307 3734.5378,-677.0328 3739.5209,-678.8305 3739.5543,-675.3307"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->sort --> +<g id="edge103" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->sort</title> +<path fill="none" stroke="#000000" d="M6001.0248,-956.1323C6395.4646,-949.1192 7464,-916.5925 7464,-770 7464,-770 7464,-770 7464,-206 7464,-132.3938 7409.7482,-122.776 7342,-94 7284.1662,-69.4351 6312.6485,-27.3767 6122.231,-19.3463"/> +<polygon fill="#000000" stroke="#000000" points="6122.1578,-17.5918 6117.0885,-19.1297 6122.0104,-21.0887 6122.1578,-17.5918"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/labels --> +<g id="edge86" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/labels</title> +<path fill="none" stroke="#000000" d="M5858.6254,-939.7901C5861.0978,-901.2892 5861.7387,-810.3776 5820,-752 5762.8732,-672.0999 5655.9127,-625.3281 5584.8115,-601.7014"/> +<polygon fill="#000000" stroke="#000000" points="5585.0518,-599.9383 5579.7553,-600.0401 5583.9592,-603.2634 5585.0518,-599.9383"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/sirupsen/logrus --> +<g id="edge96" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M5878.3689,-939.8456C5912.3733,-908.8006 5975,-841.8685 5975,-770 5975,-770 5975,-770 5975,-676 5975,-556.4411 5904.9717,-530.7548 5802,-470 5675.3558,-395.2781 5251.3659,-272.6684 5082.3839,-225.4123"/> +<polygon fill="#000000" stroke="#000000" points="5082.7459,-223.6965 5077.4594,-224.0363 5081.8039,-227.0674 5082.7459,-223.6965"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/reference --> +<g id="edge88" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/reference</title> +<path fill="none" stroke="#000000" d="M6001.2512,-942.9087C6058.1807,-932.132 6122.214,-913.6604 6174,-882 6229.9357,-847.8025 6270,-835.5612 6270,-770 6270,-770 6270,-770 6270,-676 6270,-522.7086 6071.8424,-445.7373 5953.4304,-413.4265"/> +<polygon fill="#000000" stroke="#000000" points="5953.8365,-411.7235 5948.5531,-412.1085 5952.9234,-415.1023 5953.8365,-411.7235"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->net/url --> +<g id="edge101" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->net/url</title> +<path fill="none" stroke="#000000" d="M6001.2543,-955.3986C6273.1337,-949.4027 6851.4461,-931.1484 7045,-882 7159.8704,-852.8314 7287,-888.5159 7287,-770 7287,-770 7287,-770 7287,-300 7287,-240.705 7280.9813,-171.1037 7277.4689,-135.5337"/> +<polygon fill="#000000" stroke="#000000" points="7279.1669,-134.925 7276.928,-130.124 7275.6842,-135.2733 7279.1669,-134.925"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->path --> +<g id="edge102" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->path</title> +<path fill="none" stroke="#000000" d="M5983.9901,-939.9559C6073.338,-925.734 6184.0039,-904.5893 6225,-882 6285.5651,-848.628 6333,-839.1508 6333,-770 6333,-770 6333,-770 6333,-582 6333,-432.3234 6132.9317,-341.5311 6053.2155,-311.296"/> +<polygon fill="#000000" stroke="#000000" points="6053.4383,-309.5105 6048.1422,-309.3921 6052.2085,-312.7873 6053.4383,-309.5105"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/remotes --> +<g id="edge89" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/remotes</title> +<path fill="none" stroke="#000000" d="M5712.7849,-945.9178C5302.5821,-911.5514 4133.6827,-813.6223 3742.4045,-780.8414"/> +<polygon fill="#000000" stroke="#000000" points="3742.3976,-779.0848 3737.2689,-780.4111 3742.1053,-782.5725 3742.3976,-779.0848"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1 --> +<g id="node45" class="node"> +<title>github.com/containerd/containerd/remotes/docker/schema1</title> +<g id="a_node45"><a xlink:href="https://godoc.org/github.com/containerd/containerd/remotes/docker/schema1" xlink:title="github.com/containerd/containerd/remotes/docker/schema1" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2599,-882C2599,-882 2283,-882 2283,-882 2277,-882 2271,-876 2271,-870 2271,-870 2271,-858 2271,-858 2271,-852 2277,-846 2283,-846 2283,-846 2599,-846 2599,-846 2605,-846 2611,-852 2611,-858 2611,-858 2611,-870 2611,-870 2611,-876 2605,-882 2599,-882"/> +<text text-anchor="middle" x="2441" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/remotes/docker/schema1</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/remotes/docker/schema1 --> +<g id="edge90" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/remotes/docker/schema1</title> +<path fill="none" stroke="#000000" d="M5712.9438,-954.0359C5165.3868,-938.9685 3214.2719,-885.2786 2616.4137,-868.827"/> +<polygon fill="#000000" stroke="#000000" points="2616.225,-867.0712 2611.1788,-868.6829 2616.1287,-870.5699 2616.225,-867.0712"/> +</g> +<!-- github.com/containerd/containerd/version --> +<g id="node46" class="node"> +<title>github.com/containerd/containerd/version</title> +<g id="a_node46"><a xlink:href="https://godoc.org/github.com/containerd/containerd/version" xlink:title="github.com/containerd/containerd/version" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7799,-882C7799,-882 7579,-882 7579,-882 7573,-882 7567,-876 7567,-870 7567,-870 7567,-858 7567,-858 7567,-852 7573,-846 7579,-846 7579,-846 7799,-846 7799,-846 7805,-846 7811,-852 7811,-858 7811,-858 7811,-870 7811,-870 7811,-876 7805,-882 7799,-882"/> +<text text-anchor="middle" x="7689" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containerd/containerd/version</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/version --> +<g id="edge91" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/containerd/containerd/version</title> +<path fill="none" stroke="#000000" d="M6001.1996,-955.3635C6293.0883,-949.176 6974.6468,-930.4099 7545,-882 7550.4778,-881.5351 7556.068,-881.0186 7561.7116,-880.4628"/> +<polygon fill="#000000" stroke="#000000" points="7562.1075,-882.182 7566.9079,-879.9416 7561.7582,-878.6994 7562.1075,-882.182"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode --> +<g id="node47" class="node"> +<title>github.com/docker/distribution/registry/api/errcode</title> +<g id="a_node47"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M6380.5,-130C6380.5,-130 6109.5,-130 6109.5,-130 6103.5,-130 6097.5,-124 6097.5,-118 6097.5,-118 6097.5,-106 6097.5,-106 6097.5,-100 6103.5,-94 6109.5,-94 6109.5,-94 6380.5,-94 6380.5,-94 6386.5,-94 6392.5,-100 6392.5,-106 6392.5,-106 6392.5,-118 6392.5,-118 6392.5,-124 6386.5,-130 6380.5,-130"/> +<text text-anchor="middle" x="6245" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->github.com/docker/distribution/registry/api/errcode --> +<g id="edge92" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M6001.1578,-948.0244C6169.1593,-935.3034 6436.013,-911.392 6532,-882 6634.6453,-850.5692 6746,-877.3497 6746,-770 6746,-770 6746,-770 6746,-488 6746,-323.7022 6665.2578,-281.269 6530,-188 6489.5284,-160.0922 6439.3314,-142.4278 6392.3947,-131.2487"/> +<polygon fill="#000000" stroke="#000000" points="6392.5447,-129.4866 6387.2776,-130.0523 6391.7478,-132.8947 6392.5447,-129.4866"/> +</g> +<!-- golang.org/x/net/context/ctxhttp --> +<g id="node48" class="node"> +<title>golang.org/x/net/context/ctxhttp</title> +<g id="a_node48"><a xlink:href="https://godoc.org/golang.org/x/net/context/ctxhttp" xlink:title="golang.org/x/net/context/ctxhttp" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M7171.5,-224C7171.5,-224 7004.5,-224 7004.5,-224 6998.5,-224 6992.5,-218 6992.5,-212 6992.5,-212 6992.5,-200 6992.5,-200 6992.5,-194 6998.5,-188 7004.5,-188 7004.5,-188 7171.5,-188 7171.5,-188 7177.5,-188 7183.5,-194 7183.5,-200 7183.5,-200 7183.5,-212 7183.5,-212 7183.5,-218 7177.5,-224 7171.5,-224"/> +<text text-anchor="middle" x="7088" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/context/ctxhttp</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->golang.org/x/net/context/ctxhttp --> +<g id="edge97" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->golang.org/x/net/context/ctxhttp</title> +<path fill="none" stroke="#000000" d="M6001.0717,-952.5904C6281.5208,-941.414 6881.179,-914.1155 6970,-882 7049.927,-853.1004 7125,-854.9912 7125,-770 7125,-770 7125,-770 7125,-394 7125,-333.6006 7106.3872,-264.3058 7095.5642,-229.1176"/> +<polygon fill="#000000" stroke="#000000" points="7097.1839,-228.4326 7094.027,-224.1785 7093.842,-229.4727 7097.1839,-228.4326"/> +</g> +<!-- net/http --> +<g id="node49" class="node"> +<title>net/http</title> +<g id="a_node49"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7221,-36C7221,-36 7187,-36 7187,-36 7181,-36 7175,-30 7175,-24 7175,-24 7175,-12 7175,-12 7175,-6 7181,0 7187,0 7187,0 7221,0 7221,0 7227,0 7233,-6 7233,-12 7233,-12 7233,-24 7233,-24 7233,-30 7227,-36 7221,-36"/> +<text text-anchor="middle" x="7204" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text> +</a> +</g> +</g> +<!-- github.com/containerd/containerd/remotes/docker->net/http --> +<g id="edge100" class="edge"> +<title>github.com/containerd/containerd/remotes/docker->net/http</title> +<path fill="none" stroke="#000000" d="M6001.1221,-953.4119C6338.4793,-942.2282 7160.2018,-912.2544 7282,-882 7396.6293,-853.5264 7523,-888.1127 7523,-770 7523,-770 7523,-770 7523,-206 7523,-75.0043 7320.8767,-33.434 7238.4103,-21.9248"/> +<polygon fill="#000000" stroke="#000000" points="7238.3451,-20.1499 7233.1548,-21.2095 7237.8731,-23.6179 7238.3451,-20.1499"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->bytes --> +<g id="edge107" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->bytes</title> +<path fill="none" stroke="#000000" d="M2611.1225,-862.8764C3171.65,-858.6781 4939.0521,-841.3155 5190,-788 5297.2146,-765.2216 5585.0301,-691.0851 5646,-600 5667.9018,-567.2801 5680.8066,-338.3944 5487,-188 5349.9442,-81.6444 5257.9671,-203.8725 5101,-130 5056.2041,-108.918 5015.8072,-66.4833 4993.7031,-40.3268"/> +<polygon fill="#000000" stroke="#000000" points="4994.8646,-38.9877 4990.3147,-36.2744 4992.1795,-41.2328 4994.8646,-38.9877"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->context --> +<g id="edge108" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->context</title> +<path fill="none" stroke="#000000" d="M2383.9493,-845.9228C2315.2563,-820.1691 2209,-765.8163 2209,-676 2209,-676 2209,-676 2209,-488 2209,-326.3074 2374.7576,-185.1874 2444.0822,-133.2376"/> +<polygon fill="#000000" stroke="#000000" points="2445.143,-134.6296 2448.1082,-130.2398 2443.0527,-131.8224 2445.143,-134.6296"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->encoding/base64 --> +<g id="edge109" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->encoding/base64</title> +<path fill="none" stroke="#000000" d="M2611.0782,-861.934C3220.2177,-854.2491 5272.3077,-825.8015 5565,-788 5570.5598,-787.2819 5576.306,-786.3263 5582.0335,-785.2287"/> +<polygon fill="#000000" stroke="#000000" points="5582.7203,-786.8763 5587.2822,-784.1834 5582.0366,-783.4437 5582.7203,-786.8763"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->encoding/json --> +<g id="edge110" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->encoding/json</title> +<path fill="none" stroke="#000000" d="M2611.2328,-862.9662C3197.131,-858.9428 5109.2292,-841.7903 5380,-788 5548.9825,-754.4306 5954.2319,-549.5287 6058,-412 6093.6553,-364.7444 6109.0584,-336.9347 6087,-282 6041.2977,-168.1819 5919.1822,-77.9277 5859.2035,-38.9278"/> +<polygon fill="#000000" stroke="#000000" points="5860.1439,-37.4518 5854.9944,-36.2087 5858.2447,-40.3918 5860.1439,-37.4518"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->fmt --> +<g id="edge111" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->fmt</title> +<path fill="none" stroke="#000000" d="M2270.7749,-860.9716C1867.0301,-851.5307 885,-814.3476 885,-676 885,-676 885,-676 885,-582 885,-339.9071 914.2372,-209.5031 1127,-94 1205.9227,-51.155 2621.8459,-22.949 2854.7116,-18.5906"/> +<polygon fill="#000000" stroke="#000000" points="2855.0246,-20.3351 2859.9911,-18.4921 2854.9592,-16.8357 2855.0246,-20.3351"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/archive/compression --> +<g id="edge112" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/archive/compression</title> +<path fill="none" stroke="#000000" d="M2419.6311,-845.8456C2385.6267,-814.8006 2323,-747.8685 2323,-676 2323,-676 2323,-676 2323,-582 2323,-507.9565 2394.7531,-445.8785 2438.3376,-415.0007"/> +<polygon fill="#000000" stroke="#000000" points="2439.3726,-416.4124 2442.4641,-412.1106 2437.3647,-413.5456 2439.3726,-416.4124"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/log --> +<g id="edge116" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/log</title> +<path fill="none" stroke="#000000" d="M2611.1081,-862.0235C3131.7079,-854.6093 4679.2031,-821.9243 4863,-694 4953.2437,-631.1895 4973.477,-579.8139 4968,-470 4965.3703,-417.2752 4958.1803,-355.6937 4954.0419,-323.0545"/> +<polygon fill="#000000" stroke="#000000" points="4955.7747,-322.8083 4953.4051,-318.0704 4952.303,-323.2519 4955.7747,-322.8083"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->io --> +<g id="edge123" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->io</title> +<path fill="none" stroke="#000000" d="M2270.9619,-860.435C1900.5818,-851.9604 1043.9266,-828.2536 918,-788 693.4359,-716.2161 354,-349.1122 354,-300 354,-300 354,-300 354,-206 354,-153.5763 350.4348,-127.2071 391,-94 414.4274,-74.8221 862.9207,-32.407 987.9084,-20.9183"/> +<polygon fill="#000000" stroke="#000000" points="988.0983,-22.6583 992.9174,-20.4586 987.7783,-19.173 988.0983,-22.6583"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->strconv --> +<g id="edge125" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->strconv</title> +<path fill="none" stroke="#000000" d="M2270.8846,-861.1752C1884.5002,-854.0134 963.8085,-832.473 830,-788 714.5677,-749.6345 177,-419.6046 177,-300 177,-300 177,-300 177,-206 177,-153.5763 173.6322,-127.4468 214,-94 255.8056,-59.3619 636.9615,-28.8903 752.7766,-20.3662"/> +<polygon fill="#000000" stroke="#000000" points="752.9052,-22.1115 757.7638,-20.0006 752.6492,-18.6209 752.9052,-22.1115"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->sync --> +<g id="edge127" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->sync</title> +<path fill="none" stroke="#000000" d="M2611.2905,-862.4104C3184.3968,-856.6504 5021.9665,-834.7064 5284,-788 5412.4037,-765.1126 5444.2714,-750.1367 5562,-694 5812.9731,-574.3281 5990.3274,-586.6482 6062,-318 6066.1244,-302.5407 6070.1494,-295.7691 6062,-282 6010.3506,-194.7343 5711.9208,-70.0207 5612.911,-30.5058"/> +<polygon fill="#000000" stroke="#000000" points="5613.3619,-28.8018 5608.0692,-28.578 5612.0672,-32.0536 5613.3619,-28.8018"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/content --> +<g id="edge113" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/content</title> +<path fill="none" stroke="#000000" d="M2336.5141,-845.9734C2288.4418,-834.4603 2232.1897,-816.2221 2187,-788 2107.4065,-738.2919 2039.8955,-646.5514 2011.4414,-604.3313"/> +<polygon fill="#000000" stroke="#000000" points="2012.8848,-603.3416 2008.649,-600.1602 2009.9764,-605.2887 2012.8848,-603.3416"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/errdefs --> +<g id="edge114" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/errdefs</title> +<path fill="none" stroke="#000000" d="M2479.9388,-845.9914C2615.6149,-783.2433 3067.0515,-574.4608 3210.1132,-508.297"/> +<polygon fill="#000000" stroke="#000000" points="3210.8602,-509.8797 3214.6637,-506.1924 3209.391,-506.7029 3210.8602,-509.8797"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/go-digest --> +<g id="edge118" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2292.5369,-845.9996C2050.3608,-814.5999 1597,-746.4152 1597,-676 1597,-676 1597,-676 1597,-488 1597,-410.3135 1825.0079,-348.7457 1959.6218,-319.1663"/> +<polygon fill="#000000" stroke="#000000" points="1960.2568,-320.8188 1964.7676,-318.0411 1959.5091,-317.3996 1960.2568,-320.8188"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge120" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M2270.9997,-857.4914C2035.426,-847.2012 1632.1008,-824.5093 1575,-788 1471.4008,-721.7604 1423.4754,-569.4792 1408.4532,-511.2363"/> +<polygon fill="#000000" stroke="#000000" points="1410.1077,-510.6411 1407.1823,-506.2247 1406.7151,-511.5015 1410.1077,-510.6411"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/pkg/errors --> +<g id="edge121" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2611.0946,-862.9983C3185.5056,-859.1216 5023.9932,-842.4965 5138,-788 5308.3482,-706.5717 5421.1274,-489.0367 5454.6001,-417.3046"/> +<polygon fill="#000000" stroke="#000000" points="5456.3601,-417.6683 5456.8728,-412.3957 5453.184,-416.1978 5456.3601,-417.6683"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->io/ioutil --> +<g id="edge124" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2270.7799,-860.9065C1870.4568,-852.9802 892.1737,-829.5848 749,-788 469.2076,-706.7342 384.2999,-646.9482 212,-412 123.7291,-291.6338 97.0245,-106.5293 90.1254,-41.5353"/> +<polygon fill="#000000" stroke="#000000" points="91.8436,-41.1367 89.5896,-36.3428 88.3621,-41.496 91.8436,-41.1367"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->time --> +<g id="edge128" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->time</title> +<path fill="none" stroke="#000000" d="M2270.7464,-860.2118C1917.5124,-851.5894 1128.0987,-828.0822 1012,-788 756.4859,-699.7856 649,-570.3132 649,-300 649,-300 649,-300 649,-206 649,-109.8838 745.1087,-130.5604 834,-94 885.8763,-72.6636 1047.7765,-38.6276 1117.5119,-24.4876"/> +<polygon fill="#000000" stroke="#000000" points="1118.2173,-26.1304 1122.771,-23.4235 1117.5232,-22.6999 1118.2173,-26.1304"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->strings --> +<g id="edge126" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->strings</title> +<path fill="none" stroke="#000000" d="M2611.0343,-863.2771C3207.3293,-860.2519 5182.5155,-845.9198 5461,-788 5569.4253,-765.4495 6271.2459,-448.6364 6497,-318 6600.0952,-258.3423 6655.9365,-244.7152 6688,-130 6692.307,-114.5906 6694.0243,-108.8226 6688,-94 6679.1149,-72.1384 6660.584,-53.1206 6644.2898,-39.5772"/> +<polygon fill="#000000" stroke="#000000" points="6645.186,-38.0504 6640.2027,-36.2535 6642.9778,-40.7658 6645.186,-38.0504"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/images --> +<g id="edge115" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/images</title> +<path fill="none" stroke="#000000" d="M2553.3763,-845.9738C2775.6105,-810.3253 3269.2876,-731.1348 3495.5696,-694.837"/> +<polygon fill="#000000" stroke="#000000" points="3495.8748,-696.5605 3500.5344,-694.0406 3495.3203,-693.1046 3495.8748,-696.5605"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->golang.org/x/sync/errgroup --> +<g id="edge122" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->golang.org/x/sync/errgroup</title> +<path fill="none" stroke="#000000" d="M2611.4347,-857.4778C3143.589,-836.3819 4743.8801,-767.113 4827,-694 4879.9585,-647.4173 4878.6918,-554.2691 4874.8239,-511.032"/> +<polygon fill="#000000" stroke="#000000" points="4876.562,-510.8233 4874.3428,-506.0131 4873.078,-511.1573 4876.562,-510.8233"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/remotes --> +<g id="edge117" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/containerd/containerd/remotes</title> +<path fill="none" stroke="#000000" d="M2611.1056,-850.3567C2847.7452,-831.3771 3271.1007,-797.422 3483.6981,-780.3706"/> +<polygon fill="#000000" stroke="#000000" points="3484.148,-782.0903 3488.9921,-779.946 3483.8681,-778.6015 3484.148,-782.0903"/> +</g> +<!-- github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/image-spec/specs-go --> +<g id="edge119" class="edge"> +<title>github.com/containerd/containerd/remotes/docker/schema1->github.com/opencontainers/image-spec/specs-go</title> +<path fill="none" stroke="#000000" d="M2270.7328,-848.7319C2259.9952,-847.7987 2249.349,-846.8814 2239,-846 2007.8513,-826.3132 1209,-907.9855 1209,-676 1209,-676 1209,-676 1209,-582 1209,-521.539 1228.1158,-452.2699 1239.2314,-417.102"/> +<polygon fill="#000000" stroke="#000000" points="1240.9537,-417.4613 1240.8101,-412.1658 1237.62,-416.3951 1240.9537,-417.4613"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->encoding/json --> +<g id="edge129" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->encoding/json</title> +<path fill="none" stroke="#000000" d="M6164.5167,-93.9871C6079.4982,-74.9591 5948.014,-45.5317 5876.1688,-29.4521"/> +<polygon fill="#000000" stroke="#000000" points="5876.362,-27.7021 5871.1004,-28.3177 5875.5975,-31.1176 5876.362,-27.7021"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->fmt --> +<g id="edge130" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->fmt</title> +<path fill="none" stroke="#000000" d="M6097.2268,-108.3051C5679.3357,-97.791 4453.297,-66.4942 3435,-36 3239.0548,-30.1322 3003.7351,-22.0566 2919.1929,-19.1226"/> +<polygon fill="#000000" stroke="#000000" points="2919.1405,-17.3699 2914.0828,-18.9452 2919.019,-20.8677 2919.1405,-17.3699"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sync --> +<g id="edge134" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sync</title> +<path fill="none" stroke="#000000" d="M6117.7598,-93.9871C5960.659,-71.7469 5703.2024,-35.2997 5613.2394,-22.564"/> +<polygon fill="#000000" stroke="#000000" points="5613.2606,-20.7996 5608.0647,-21.8314 5612.77,-24.2651 5613.2606,-20.7996"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->strings --> +<g id="edge133" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->strings</title> +<path fill="none" stroke="#000000" d="M6315.7103,-93.9871C6395.7464,-73.5985 6522.653,-41.27 6581.7476,-26.216"/> +<polygon fill="#000000" stroke="#000000" points="6582.4966,-27.8312 6586.9098,-24.901 6581.6325,-24.4395 6582.4966,-27.8312"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sort --> +<g id="edge132" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sort</title> +<path fill="none" stroke="#000000" d="M6215.1144,-93.8759C6188.2262,-77.5694 6148.8535,-53.6918 6121.4843,-37.0937"/> +<polygon fill="#000000" stroke="#000000" points="6122.3812,-35.591 6117.1985,-34.4946 6120.5663,-38.5837 6122.3812,-35.591"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->net/http --> +<g id="edge131" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->net/http</title> +<path fill="none" stroke="#000000" d="M6392.5053,-98.5746C6552.3458,-83.8729 6814.8726,-59.2793 7041,-36 7085.491,-31.4198 7136.7591,-25.6826 7169.7326,-21.9332"/> +<polygon fill="#000000" stroke="#000000" points="7170.0938,-23.6535 7174.8637,-21.349 7169.6978,-20.1759 7170.0938,-23.6535"/> +</g> +<!-- golang.org/x/net/context/ctxhttp->context --> +<g id="edge202" class="edge"> +<title>golang.org/x/net/context/ctxhttp->context</title> +<path fill="none" stroke="#000000" d="M6992.4249,-204.0529C6368.0903,-191.3334 2884.7092,-120.3673 2507.1831,-112.676"/> +<polygon fill="#000000" stroke="#000000" points="2507.1426,-110.9249 2502.108,-112.5726 2507.0712,-114.4242 2507.1426,-110.9249"/> +</g> +<!-- golang.org/x/net/context/ctxhttp->io --> +<g id="edge203" class="edge"> +<title>golang.org/x/net/context/ctxhttp->io</title> +<path fill="none" stroke="#000000" d="M6992.231,-203.9498C6611.3193,-195.7338 5166.7286,-163.9911 3978,-130 3818.5352,-125.4402 1266.4346,-54.6623 1108,-36 1089.3333,-33.8012 1068.7366,-29.6827 1052.1652,-25.9397"/> +<polygon fill="#000000" stroke="#000000" points="1052.4228,-24.2034 1047.1582,-24.7926 1051.6411,-27.615 1052.4228,-24.2034"/> +</g> +<!-- golang.org/x/net/context/ctxhttp->strings --> +<g id="edge206" class="edge"> +<title>golang.org/x/net/context/ctxhttp->strings</title> +<path fill="none" stroke="#000000" d="M7099.2702,-187.7532C7112.6941,-163.4277 7130.6356,-120.3844 7108,-94 7077.6837,-58.663 6750.973,-29.1584 6646.1626,-20.5446"/> +<polygon fill="#000000" stroke="#000000" points="6646.1726,-18.7897 6641.0466,-20.1263 6645.8873,-22.2781 6646.1726,-18.7897"/> +</g> +<!-- golang.org/x/net/context/ctxhttp->net/url --> +<g id="edge205" class="edge"> +<title>golang.org/x/net/context/ctxhttp->net/url</title> +<path fill="none" stroke="#000000" d="M7124.0555,-187.8759C7158.5503,-170.5362 7210.0763,-144.6354 7243.1106,-128.03"/> +<polygon fill="#000000" stroke="#000000" points="7244.1647,-129.4588 7247.8461,-125.6496 7242.5927,-126.3317 7244.1647,-129.4588"/> +</g> +<!-- golang.org/x/net/context/ctxhttp->net/http --> +<g id="edge204" class="edge"> +<title>golang.org/x/net/context/ctxhttp->net/http</title> +<path fill="none" stroke="#000000" d="M7112.4793,-187.8148C7129.8694,-173.7517 7152.601,-152.8695 7167,-130 7184.5484,-102.1283 7194.6513,-65.0699 7199.7374,-41.3696"/> +<polygon fill="#000000" stroke="#000000" points="7201.5013,-41.4829 7200.8064,-36.2312 7198.0746,-40.7699 7201.5013,-41.4829"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go->fmt --> +<g id="edge177" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go->fmt</title> +<path fill="none" stroke="#000000" d="M1177.1337,-375.8811C1147.6604,-364.4067 1116.2357,-346.2262 1099,-318 1053.5978,-243.6469 1034.2573,-194.405 1179,-94 1250.4981,-44.4032 2624.4964,-21.8944 2854.4781,-18.4675"/> +<polygon fill="#000000" stroke="#000000" points="2854.7233,-20.2142 2859.6968,-18.3901 2854.6714,-16.7146 2854.7233,-20.2142"/> +</g> +<!-- github.com/golang/protobuf/proto->bufio --> +<g id="edge135" class="edge"> +<title>github.com/golang/protobuf/proto->bufio</title> +<path fill="none" stroke="#000000" d="M4176.7828,-93.9871C4273.0097,-73.0488 4427.1078,-39.5182 4493.6607,-25.0368"/> +<polygon fill="#000000" stroke="#000000" points="4494.1679,-26.7175 4498.6815,-23.9443 4493.4237,-23.2975 4494.1679,-26.7175"/> +</g> +<!-- github.com/golang/protobuf/proto->bytes --> +<g id="edge136" class="edge"> +<title>github.com/golang/protobuf/proto->bytes</title> +<path fill="none" stroke="#000000" d="M4195.1547,-101.969C4336.2798,-87.8636 4600.2349,-61.0793 4825,-36 4865.9252,-31.4336 4913.0355,-25.7496 4943.6014,-22.005"/> +<polygon fill="#000000" stroke="#000000" points="4943.986,-23.721 4948.7357,-21.3752 4943.5598,-20.2471 4943.986,-23.721"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding --> +<g id="edge137" class="edge"> +<title>github.com/golang/protobuf/proto->encoding</title> +<path fill="none" stroke="#000000" d="M4077.6111,-93.8759C4063.7355,-78.531 4043.7971,-56.4815 4028.9866,-40.1029"/> +<polygon fill="#000000" stroke="#000000" points="4029.9901,-38.6034 4025.3385,-36.0685 4027.3941,-40.9509 4029.9901,-38.6034"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding/json --> +<g id="edge138" class="edge"> +<title>github.com/golang/protobuf/proto->encoding/json</title> +<path fill="none" stroke="#000000" d="M4195.2794,-106.7722C4265.6178,-103.1535 4361.51,-98.2438 4446,-94 4968.6431,-67.7487 5099.9382,-72.0057 5622,-36 5673.6208,-32.4398 5732.5117,-27.073 5773.5199,-23.1255"/> +<polygon fill="#000000" stroke="#000000" points="5773.873,-24.8497 5778.6816,-22.6271 5773.5365,-21.3659 5773.873,-24.8497"/> +</g> +<!-- github.com/golang/protobuf/proto->errors --> +<g id="edge139" class="edge"> +<title>github.com/golang/protobuf/proto->errors</title> +<path fill="none" stroke="#000000" d="M4136.7328,-93.9871C4180.1184,-75.699 4246.2937,-47.8045 4285.2417,-31.3869"/> +<polygon fill="#000000" stroke="#000000" points="4285.9255,-32.9979 4289.8531,-29.4431 4284.5659,-29.7727 4285.9255,-32.9979"/> +</g> +<!-- github.com/golang/protobuf/proto->fmt --> +<g id="edge140" class="edge"> +<title>github.com/golang/protobuf/proto->fmt</title> +<path fill="none" stroke="#000000" d="M3992.9094,-104.1272C3737.0964,-84.2047 3073.4421,-32.5199 2919.3089,-20.5162"/> +<polygon fill="#000000" stroke="#000000" points="2919.1631,-18.7496 2914.0423,-20.106 2918.8913,-22.2391 2919.1631,-18.7496"/> +</g> +<!-- github.com/golang/protobuf/proto->io --> +<g id="edge141" class="edge"> +<title>github.com/golang/protobuf/proto->io</title> +<path fill="none" stroke="#000000" d="M3992.9379,-103.2442C3949.8469,-99.8242 3898.9949,-96.202 3853,-94 3243.5619,-64.8233 1714.3512,-103.8555 1108,-36 1089.3208,-33.9097 1068.7234,-29.7975 1052.1547,-26.0308"/> +<polygon fill="#000000" stroke="#000000" points="1052.4142,-24.2947 1047.1487,-24.8755 1051.6271,-27.7051 1052.4142,-24.2947"/> +</g> +<!-- github.com/golang/protobuf/proto->os --> +<g id="edge144" class="edge"> +<title>github.com/golang/protobuf/proto->os</title> +<path fill="none" stroke="#000000" d="M4032.2962,-93.9871C3964.187,-74.1043 3857.1816,-42.8667 3804.0901,-27.3679"/> +<polygon fill="#000000" stroke="#000000" points="3804.4137,-25.6394 3799.1236,-25.9181 3803.4328,-28.9992 3804.4137,-25.6394"/> +</g> +<!-- github.com/golang/protobuf/proto->strconv --> +<g id="edge147" class="edge"> +<title>github.com/golang/protobuf/proto->strconv</title> +<path fill="none" stroke="#000000" d="M3992.7883,-110.4741C3526.8132,-103.3042 1584.8739,-71.8052 978,-36 922.1856,-32.707 857.4845,-26.0503 819.2436,-21.8198"/> +<polygon fill="#000000" stroke="#000000" points="819.4188,-20.0786 814.256,-21.2652 819.032,-23.5572 819.4188,-20.0786"/> +</g> +<!-- github.com/golang/protobuf/proto->sync --> +<g id="edge149" class="edge"> +<title>github.com/golang/protobuf/proto->sync</title> +<path fill="none" stroke="#000000" d="M4195.0385,-105.6129C4494.8984,-86.6574 5369.986,-31.3391 5548.8464,-20.0326"/> +<polygon fill="#000000" stroke="#000000" points="5549.0542,-21.773 5553.9338,-19.711 5548.8333,-18.28 5549.0542,-21.773"/> +</g> +<!-- github.com/golang/protobuf/proto->strings --> +<g id="edge148" class="edge"> +<title>github.com/golang/protobuf/proto->strings</title> +<path fill="none" stroke="#000000" d="M4195.2609,-106.3734C4265.5909,-102.5754 4361.4808,-97.6154 4446,-94 5194.6478,-61.9759 5382.1485,-62.8419 6131,-36 6300.5187,-29.9237 6503.4865,-22.22 6581.3769,-19.2478"/> +<polygon fill="#000000" stroke="#000000" points="6581.8371,-20.9816 6586.7667,-19.0421 6581.7036,-17.4841 6581.8371,-20.9816"/> +</g> +<!-- github.com/golang/protobuf/proto->sort --> +<g id="edge146" class="edge"> +<title>github.com/golang/protobuf/proto->sort</title> +<path fill="none" stroke="#000000" d="M4195.2679,-106.531C4265.601,-102.8039 4361.4918,-97.8637 4446,-94 5085.4069,-64.7665 5246.2328,-76.8935 5885,-36 5946.0787,-32.0898 6017.2106,-25.3525 6057.5904,-21.3227"/> +<polygon fill="#000000" stroke="#000000" points="6058.039,-23.0366 6062.8396,-20.7968 6057.69,-19.5541 6058.039,-23.0366"/> +</g> +<!-- github.com/golang/protobuf/proto->sync/atomic --> +<g id="edge150" class="edge"> +<title>github.com/golang/protobuf/proto->sync/atomic</title> +<path fill="none" stroke="#000000" d="M4195.0543,-97.9273C4343.3062,-77.2818 4613.8806,-39.6018 4722.2721,-24.5073"/> +<polygon fill="#000000" stroke="#000000" points="4722.5991,-26.2287 4727.3099,-23.8057 4722.1163,-22.7622 4722.5991,-26.2287"/> +</g> +<!-- github.com/golang/protobuf/proto->log --> +<g id="edge142" class="edge"> +<title>github.com/golang/protobuf/proto->log</title> +<path fill="none" stroke="#000000" d="M4060.451,-93.8759C4029.0131,-76.8921 3982.372,-51.6952 3951.5869,-35.0642"/> +<polygon fill="#000000" stroke="#000000" points="3952.3945,-33.5115 3947.1636,-32.6746 3950.7309,-36.5909 3952.3945,-33.5115"/> +</g> +<!-- math --> +<g id="node53" class="node"> +<title>math</title> +<g id="a_node53"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3492,-36C3492,-36 3462,-36 3462,-36 3456,-36 3450,-30 3450,-24 3450,-24 3450,-12 3450,-12 3450,-6 3456,0 3462,0 3462,0 3492,0 3492,0 3498,0 3504,-6 3504,-12 3504,-12 3504,-24 3504,-24 3504,-30 3498,-36 3492,-36"/> +<text text-anchor="middle" x="3477" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/proto->math --> +<g id="edge143" class="edge"> +<title>github.com/golang/protobuf/proto->math</title> +<path fill="none" stroke="#000000" d="M3992.8059,-96.5831C3850.3459,-74.8793 3598.1144,-36.4518 3509.1521,-22.8984"/> +<polygon fill="#000000" stroke="#000000" points="3509.2388,-21.1415 3504.0322,-22.1184 3508.7116,-24.6015 3509.2388,-21.1415"/> +</g> +<!-- github.com/golang/protobuf/proto->reflect --> +<g id="edge145" class="edge"> +<title>github.com/golang/protobuf/proto->reflect</title> +<path fill="none" stroke="#000000" d="M4116.9444,-93.8759C4136.6996,-78.2709 4165.2327,-55.7321 4186.0668,-39.275"/> +<polygon fill="#000000" stroke="#000000" points="4187.2872,-40.5411 4190.126,-36.0685 4185.1177,-37.7946 4187.2872,-40.5411"/> +</g> +<!-- unicode/utf8 --> +<g id="node55" class="node"> +<title>unicode/utf8</title> +<g id="a_node55"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4144.5,-36C4144.5,-36 4083.5,-36 4083.5,-36 4077.5,-36 4071.5,-30 4071.5,-24 4071.5,-24 4071.5,-12 4071.5,-12 4071.5,-6 4077.5,0 4083.5,0 4083.5,0 4144.5,0 4144.5,0 4150.5,0 4156.5,-6 4156.5,-12 4156.5,-12 4156.5,-24 4156.5,-24 4156.5,-30 4150.5,-36 4144.5,-36"/> +<text text-anchor="middle" x="4114" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/proto->unicode/utf8 --> +<g id="edge151" class="edge"> +<title>github.com/golang/protobuf/proto->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M4097.8562,-93.8759C4101.0381,-78.9211 4105.5748,-57.5983 4109.0292,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="4110.8267,-41.3233 4110.1556,-36.0685 4107.4033,-40.5948 4110.8267,-41.3233"/> +</g> +<!-- unsafe --> +<g id="node56" class="node"> +<title>unsafe</title> +<g id="a_node56"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5135,-36C5135,-36 5105,-36 5105,-36 5099,-36 5093,-30 5093,-24 5093,-24 5093,-12 5093,-12 5093,-6 5099,0 5105,0 5105,0 5135,0 5135,0 5141,0 5147,-6 5147,-12 5147,-12 5147,-24 5147,-24 5147,-30 5141,-36 5135,-36"/> +<text text-anchor="middle" x="5120" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/proto->unsafe --> +<g id="edge152" class="edge"> +<title>github.com/golang/protobuf/proto->unsafe</title> +<path fill="none" stroke="#000000" d="M4195.0417,-106.6925C4365.1633,-97.0829 4719.3732,-74.1794 5017,-36 5040.9022,-32.9338 5067.7118,-28.2074 5087.9862,-24.3671"/> +<polygon fill="#000000" stroke="#000000" points="5088.4109,-26.0678 5092.9942,-23.4114 5087.7547,-22.6298 5088.4109,-26.0678"/> +</g> +<!-- github.com/golang/protobuf/ptypes->errors --> +<g id="edge153" class="edge"> +<title>github.com/golang/protobuf/ptypes->errors</title> +<path fill="none" stroke="#000000" d="M3695.5975,-284.3578C3701.1444,-283.554 3706.6384,-282.7638 3712,-282 3901.1704,-255.0527 3969.8887,-314.8306 4138,-224 4218.8394,-180.3224 4279.9242,-84.6004 4304.7921,-40.7527"/> +<polygon fill="#000000" stroke="#000000" points="4306.4426,-41.3877 4307.3654,-36.1713 4303.391,-39.6737 4306.4426,-41.3877"/> +</g> +<!-- github.com/golang/protobuf/ptypes->fmt --> +<g id="edge154" class="edge"> +<title>github.com/golang/protobuf/ptypes->fmt</title> +<path fill="none" stroke="#000000" d="M3486.3127,-291.2957C3363.0533,-279.7477 3166.7759,-256.9115 3101,-224 3027.0788,-187.0129 3027.6982,-151.171 2968,-94 2948.7416,-75.5569 2926.6548,-54.8634 2910.4142,-39.732"/> +<polygon fill="#000000" stroke="#000000" points="2911.2684,-38.1361 2906.4165,-36.0096 2908.8833,-40.6977 2911.2684,-38.1361"/> +</g> +<!-- github.com/golang/protobuf/ptypes->time --> +<g id="edge161" class="edge"> +<title>github.com/golang/protobuf/ptypes->time</title> +<path fill="none" stroke="#000000" d="M3486.4343,-288.7772C3375.768,-276.1355 3196.2766,-253.451 3043,-224 2806.0903,-178.4796 2754.5805,-129.7445 2516,-94 2246.4702,-53.6187 1362.4646,-24.5627 1182.2478,-18.9773"/> +<polygon fill="#000000" stroke="#000000" points="1182.1742,-17.2243 1177.1225,-18.8189 1182.0661,-20.7226 1182.1742,-17.2243"/> +</g> +<!-- github.com/golang/protobuf/ptypes->strings --> +<g id="edge160" class="edge"> +<title>github.com/golang/protobuf/ptypes->strings</title> +<path fill="none" stroke="#000000" d="M3695.5238,-283.7344C3701.0898,-283.0924 3706.6081,-282.5075 3712,-282 4353.6799,-221.6062 4519.302,-284.2016 5161,-224 5269.7014,-213.8021 5295.3776,-199.0083 5404,-188 5637.6019,-164.3257 6237.6678,-208.3733 6459,-130 6513.2111,-110.804 6565.026,-66.306 6592.8665,-39.5363"/> +<polygon fill="#000000" stroke="#000000" points="6594.0988,-40.7791 6596.4696,-36.0417 6591.662,-38.2667 6594.0988,-40.7791"/> +</g> +<!-- github.com/golang/protobuf/ptypes->github.com/golang/protobuf/proto --> +<g id="edge155" class="edge"> +<title>github.com/golang/protobuf/ptypes->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3695.6064,-284.419C3701.1509,-283.5993 3706.6421,-282.7889 3712,-282 3800.4252,-268.9799 4051.534,-291.6038 4110,-224 4131.2,-199.4866 4118.4252,-160.0454 4106.6642,-135.1026"/> +<polygon fill="#000000" stroke="#000000" points="4108.0968,-134.0469 4104.3359,-130.3161 4104.9494,-135.5779 4108.0968,-134.0469"/> +</g> +<!-- github.com/golang/protobuf/ptypes->reflect --> +<g id="edge159" class="edge"> +<title>github.com/golang/protobuf/ptypes->reflect</title> +<path fill="none" stroke="#000000" d="M3695.6636,-283.8957C3813.285,-265.2958 3989.2365,-235.7502 3999,-224 4036.404,-178.9853 3943.0747,-140.9639 3978,-94 4031.4484,-22.128 4086.8142,-66.5788 4171,-36 4174.1317,-34.8625 4177.3663,-33.6137 4180.5858,-32.3208"/> +<polygon fill="#000000" stroke="#000000" points="4181.6508,-33.7759 4185.6155,-30.2624 4180.3251,-30.5366 4181.6508,-33.7759"/> +</g> +<!-- github.com/golang/protobuf/ptypes/any --> +<g id="node58" class="node"> +<title>github.com/golang/protobuf/ptypes/any</title> +<g id="a_node58"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/any" xlink:title="github.com/golang/protobuf/ptypes/any" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3973,-224C3973,-224 3765,-224 3765,-224 3759,-224 3753,-218 3753,-212 3753,-212 3753,-200 3753,-200 3753,-194 3759,-188 3765,-188 3765,-188 3973,-188 3973,-188 3979,-188 3985,-194 3985,-200 3985,-200 3985,-212 3985,-212 3985,-218 3979,-224 3973,-224"/> +<text text-anchor="middle" x="3869" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/any</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/any --> +<g id="edge156" class="edge"> +<title>github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/any</title> +<path fill="none" stroke="#000000" d="M3644.2723,-281.9871C3691.829,-265.9068 3761.3507,-242.3994 3810.4501,-225.7975"/> +<polygon fill="#000000" stroke="#000000" points="3811.2588,-227.3714 3815.4348,-224.112 3810.1377,-224.0558 3811.2588,-227.3714"/> +</g> +<!-- github.com/golang/protobuf/ptypes/duration --> +<g id="node59" class="node"> +<title>github.com/golang/protobuf/ptypes/duration</title> +<g id="a_node59"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/duration" xlink:title="github.com/golang/protobuf/ptypes/duration" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3412.5,-224C3412.5,-224 3179.5,-224 3179.5,-224 3173.5,-224 3167.5,-218 3167.5,-212 3167.5,-212 3167.5,-200 3167.5,-200 3167.5,-194 3173.5,-188 3179.5,-188 3179.5,-188 3412.5,-188 3412.5,-188 3418.5,-188 3424.5,-194 3424.5,-200 3424.5,-200 3424.5,-212 3424.5,-212 3424.5,-218 3418.5,-224 3412.5,-224"/> +<text text-anchor="middle" x="3296" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/duration</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/duration --> +<g id="edge157" class="edge"> +<title>github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/duration</title> +<path fill="none" stroke="#000000" d="M3534.4701,-281.9871C3483.8223,-265.8484 3409.6969,-242.2288 3357.5644,-225.6171"/> +<polygon fill="#000000" stroke="#000000" points="3357.8848,-223.8826 3352.5895,-224.0319 3356.8222,-227.2174 3357.8848,-223.8826"/> +</g> +<!-- github.com/golang/protobuf/ptypes/timestamp --> +<g id="node60" class="node"> +<title>github.com/golang/protobuf/ptypes/timestamp</title> +<g id="a_node60"><a xlink:href="https://godoc.org/github.com/golang/protobuf/ptypes/timestamp" xlink:title="github.com/golang/protobuf/ptypes/timestamp" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3712,-224C3712,-224 3466,-224 3466,-224 3460,-224 3454,-218 3454,-212 3454,-212 3454,-200 3454,-200 3454,-194 3460,-188 3466,-188 3466,-188 3712,-188 3712,-188 3718,-188 3724,-194 3724,-200 3724,-200 3724,-212 3724,-212 3724,-218 3718,-224 3712,-224"/> +<text text-anchor="middle" x="3589" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/ptypes/timestamp</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/timestamp --> +<g id="edge158" class="edge"> +<title>github.com/golang/protobuf/ptypes->github.com/golang/protobuf/ptypes/timestamp</title> +<path fill="none" stroke="#000000" d="M3590.6144,-281.8759C3590.2962,-266.9211 3589.8425,-245.5983 3589.4971,-229.3629"/> +<polygon fill="#000000" stroke="#000000" points="3591.2405,-229.0301 3589.3844,-224.0685 3587.7413,-229.1046 3591.2405,-229.0301"/> +</g> +<!-- github.com/golang/protobuf/ptypes/any->fmt --> +<g id="edge162" class="edge"> +<title>github.com/golang/protobuf/ptypes/any->fmt</title> +<path fill="none" stroke="#000000" d="M3774.8417,-187.9738C3562.0347,-147.2327 3052.1674,-49.6207 2919.1708,-24.159"/> +<polygon fill="#000000" stroke="#000000" points="2919.4687,-22.4343 2914.2288,-23.2128 2918.8105,-25.8719 2919.4687,-22.4343"/> +</g> +<!-- github.com/golang/protobuf/ptypes/any->github.com/golang/protobuf/proto --> +<g id="edge163" class="edge"> +<title>github.com/golang/protobuf/ptypes/any->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3912.116,-187.9871C3950.3729,-172.0042 4006.1924,-148.6841 4045.8881,-132.1001"/> +<polygon fill="#000000" stroke="#000000" points="4046.7079,-133.6542 4050.6469,-130.112 4045.3587,-130.4247 4046.7079,-133.6542"/> +</g> +<!-- github.com/golang/protobuf/ptypes/any->math --> +<g id="edge164" class="edge"> +<title>github.com/golang/protobuf/ptypes/any->math</title> +<path fill="none" stroke="#000000" d="M3758.1207,-187.9454C3660.9389,-171.205 3532.3543,-146.4691 3514,-130 3488.6713,-107.2729 3480.6352,-66.8571 3478.1141,-41.2943"/> +<polygon fill="#000000" stroke="#000000" points="3479.8367,-40.9042 3477.6556,-36.0766 3476.3501,-41.2107 3479.8367,-40.9042"/> +</g> +<!-- github.com/golang/protobuf/ptypes/duration->fmt --> +<g id="edge165" class="edge"> +<title>github.com/golang/protobuf/ptypes/duration->fmt</title> +<path fill="none" stroke="#000000" d="M3276.5078,-187.9471C3249.0971,-163.4444 3196.7066,-119.768 3145,-94 3068.4328,-55.8428 2969.3189,-33.376 2919.1446,-23.6845"/> +<polygon fill="#000000" stroke="#000000" points="2919.3908,-21.95 2914.1512,-22.7311 2918.7343,-25.3878 2919.3908,-21.95"/> +</g> +<!-- github.com/golang/protobuf/ptypes/duration->github.com/golang/protobuf/proto --> +<g id="edge166" class="edge"> +<title>github.com/golang/protobuf/ptypes/duration->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3424.6181,-189.7505C3429.4732,-189.1561 3434.2802,-188.5709 3439,-188 3632.3658,-164.6125 3858.0264,-138.7421 3987.6421,-124.0198"/> +<polygon fill="#000000" stroke="#000000" points="3988.2058,-125.7171 3992.9764,-123.414 3987.8108,-122.2395 3988.2058,-125.7171"/> +</g> +<!-- github.com/golang/protobuf/ptypes/duration->math --> +<g id="edge167" class="edge"> +<title>github.com/golang/protobuf/ptypes/duration->math</title> +<path fill="none" stroke="#000000" d="M3313.355,-187.9738C3346.6499,-153.3913 3419.3959,-77.8319 3455.6209,-40.2059"/> +<polygon fill="#000000" stroke="#000000" points="3457.2244,-41.0636 3459.4316,-36.2479 3454.703,-38.6361 3457.2244,-41.0636"/> +</g> +<!-- github.com/golang/protobuf/ptypes/timestamp->fmt --> +<g id="edge168" class="edge"> +<title>github.com/golang/protobuf/ptypes/timestamp->fmt</title> +<path fill="none" stroke="#000000" d="M3528.2612,-187.9465C3451.2054,-165.2147 3314.1158,-125.3452 3196,-94 3095.4458,-67.3153 2975.622,-38.7643 2919.2907,-25.5325"/> +<polygon fill="#000000" stroke="#000000" points="2919.6336,-23.8155 2914.366,-24.3768 2918.8339,-27.2229 2919.6336,-23.8155"/> +</g> +<!-- github.com/golang/protobuf/ptypes/timestamp->github.com/golang/protobuf/proto --> +<g id="edge169" class="edge"> +<title>github.com/golang/protobuf/ptypes/timestamp->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3685.7716,-187.9871C3773.61,-171.637 3902.6991,-147.6085 3992.0991,-130.9677"/> +<polygon fill="#000000" stroke="#000000" points="3992.5311,-132.6674 3997.1264,-130.0319 3991.8906,-129.2265 3992.5311,-132.6674"/> +</g> +<!-- github.com/golang/protobuf/ptypes/timestamp->math --> +<g id="edge170" class="edge"> +<title>github.com/golang/protobuf/ptypes/timestamp->math</title> +<path fill="none" stroke="#000000" d="M3529.4038,-187.9228C3501.6066,-176.1651 3471.1536,-157.7326 3455,-130 3438.8238,-102.2286 3451.9746,-64.6916 3463.7853,-40.9097"/> +<polygon fill="#000000" stroke="#000000" points="3465.4018,-41.5922 3466.1205,-36.3437 3462.2857,-39.9985 3465.4018,-41.5922"/> +</g> +<!-- golang.org/x/sys/unix->bytes --> +<g id="edge212" class="edge"> +<title>golang.org/x/sys/unix->bytes</title> +<path fill="none" stroke="#000000" d="M5184.5603,-93.9871C5132.5726,-75.0458 5052.3014,-45.7998 5008.038,-29.6728"/> +<polygon fill="#000000" stroke="#000000" points="5008.3815,-27.9354 5003.0845,-27.868 5007.1833,-31.224 5008.3815,-27.9354"/> +</g> +<!-- golang.org/x/sys/unix->sync --> +<g id="edge216" class="edge"> +<title>golang.org/x/sys/unix->sync</title> +<path fill="none" stroke="#000000" d="M5300.4945,-93.9871C5374.9947,-73.8055 5492.6806,-41.9251 5548.9584,-26.6799"/> +<polygon fill="#000000" stroke="#000000" points="5549.5148,-28.3423 5553.8833,-25.3457 5548.5996,-24.964 5549.5148,-28.3423"/> +</g> +<!-- golang.org/x/sys/unix->time --> +<g id="edge218" class="edge"> +<title>golang.org/x/sys/unix->time</title> +<path fill="none" stroke="#000000" d="M5166.7587,-110.0986C5045.778,-106.6999 4783.3878,-99.4306 4562,-94 3161.8919,-59.6555 1438.439,-23.9405 1182.4022,-18.666"/> +<polygon fill="#000000" stroke="#000000" points="1182.3594,-16.9149 1177.3244,-18.5615 1182.2873,-20.4142 1182.3594,-16.9149"/> +</g> +<!-- golang.org/x/sys/unix->strings --> +<g id="edge215" class="edge"> +<title>golang.org/x/sys/unix->strings</title> +<path fill="none" stroke="#000000" d="M5301.0266,-107.4344C5548.3659,-90.5867 6404.6596,-32.2594 6581.7837,-20.1944"/> +<polygon fill="#000000" stroke="#000000" points="6581.9552,-21.9369 6586.8246,-19.8511 6581.7172,-18.445 6581.9552,-21.9369"/> +</g> +<!-- golang.org/x/sys/unix->sort --> +<g id="edge214" class="edge"> +<title>golang.org/x/sys/unix->sort</title> +<path fill="none" stroke="#000000" d="M5301.1689,-104.624C5474.5471,-85.5848 5932.3109,-35.3163 6057.7936,-21.5367"/> +<polygon fill="#000000" stroke="#000000" points="6058.0395,-23.2703 6062.8186,-20.9849 6057.6574,-19.7912 6058.0395,-23.2703"/> +</g> +<!-- golang.org/x/sys/unix->runtime --> +<g id="edge213" class="edge"> +<title>golang.org/x/sys/unix->runtime</title> +<path fill="none" stroke="#000000" d="M5301.2263,-108.4156C5593.2753,-92.8441 6746.8562,-31.3372 6962.26,-19.8523"/> +<polygon fill="#000000" stroke="#000000" points="6962.571,-21.5883 6967.4707,-19.5744 6962.3846,-18.0932 6962.571,-21.5883"/> +</g> +<!-- golang.org/x/sys/unix->unsafe --> +<g id="edge219" class="edge"> +<title>golang.org/x/sys/unix->unsafe</title> +<path fill="none" stroke="#000000" d="M5212.0197,-93.8759C5193.0945,-78.2709 5165.7603,-55.7321 5145.8016,-39.275"/> +<polygon fill="#000000" stroke="#000000" points="5146.8839,-37.8992 5141.9129,-36.0685 5144.6573,-40.5997 5146.8839,-37.8992"/> +</g> +<!-- syscall --> +<g id="node63" class="node"> +<title>syscall</title> +<g id="a_node63"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5249,-36C5249,-36 5219,-36 5219,-36 5213,-36 5207,-30 5207,-24 5207,-24 5207,-12 5207,-12 5207,-6 5213,0 5219,0 5219,0 5249,0 5249,0 5255,0 5261,-6 5261,-12 5261,-12 5261,-24 5261,-24 5261,-30 5255,-36 5249,-36"/> +<text text-anchor="middle" x="5234" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text> +</a> +</g> +</g> +<!-- golang.org/x/sys/unix->syscall --> +<g id="edge217" class="edge"> +<title>golang.org/x/sys/unix->syscall</title> +<path fill="none" stroke="#000000" d="M5234,-93.8759C5234,-78.9211 5234,-57.5983 5234,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="5235.7501,-41.0685 5234,-36.0685 5232.2501,-41.0685 5235.7501,-41.0685"/> +</g> +<!-- google.golang.org/genproto/googleapis/rpc/status->fmt --> +<g id="edge220" class="edge"> +<title>google.golang.org/genproto/googleapis/rpc/status->fmt</title> +<path fill="none" stroke="#000000" d="M3726.416,-283.7958C3720.8731,-283.1857 3715.3855,-282.5852 3710,-282 3462.5626,-255.1119 3380.8743,-324.1076 3153,-224 3113.0746,-206.4603 3040.815,-120.2601 3006,-94 2977.6271,-72.5991 2943.2254,-51.0633 2918.7229,-36.409"/> +<polygon fill="#000000" stroke="#000000" points="2919.4578,-34.8099 2914.2666,-33.7544 2917.6666,-37.8168 2919.4578,-34.8099"/> +</g> +<!-- google.golang.org/genproto/googleapis/rpc/status->github.com/golang/protobuf/proto --> +<g id="edge221" class="edge"> +<title>google.golang.org/genproto/googleapis/rpc/status->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M4005.2046,-281.9926C4074.3404,-269.7648 4148.087,-250.8716 4169,-224 4192.5621,-193.7245 4155.1902,-156.2949 4125.2637,-133.2895"/> +<polygon fill="#000000" stroke="#000000" points="4125.9553,-131.6189 4120.9105,-130.0025 4123.8462,-134.412 4125.9553,-131.6189"/> +</g> +<!-- google.golang.org/genproto/googleapis/rpc/status->math --> +<g id="edge223" class="edge"> +<title>google.golang.org/genproto/googleapis/rpc/status->math</title> +<path fill="none" stroke="#000000" d="M3918.4955,-281.9479C3967.3022,-261.4267 4030.3767,-225.8368 3999,-188 3910.0833,-80.7765 3822.5905,-175.6838 3691,-130 3622.5836,-106.2481 3549.0808,-63.5689 3508.4932,-38.3109"/> +<polygon fill="#000000" stroke="#000000" points="3509.357,-36.787 3504.1895,-35.621 3507.5019,-39.755 3509.357,-36.787"/> +</g> +<!-- google.golang.org/genproto/googleapis/rpc/status->github.com/golang/protobuf/ptypes/any --> +<g id="edge222" class="edge"> +<title>google.golang.org/genproto/googleapis/rpc/status->github.com/golang/protobuf/ptypes/any</title> +<path fill="none" stroke="#000000" d="M3869,-281.8759C3869,-266.9211 3869,-245.5983 3869,-229.3629"/> +<polygon fill="#000000" stroke="#000000" points="3870.7501,-229.0685 3869,-224.0685 3867.2501,-229.0685 3870.7501,-229.0685"/> +</g> +<!-- google.golang.org/grpc/connectivity --> +<g id="node65" class="node"> +<title>google.golang.org/grpc/connectivity</title> +<g id="a_node65"><a xlink:href="https://godoc.org/google.golang.org/grpc/connectivity" xlink:title="google.golang.org/grpc/connectivity" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1839,-224C1839,-224 1649,-224 1649,-224 1643,-224 1637,-218 1637,-212 1637,-212 1637,-200 1637,-200 1637,-194 1643,-188 1649,-188 1649,-188 1839,-188 1839,-188 1845,-188 1851,-194 1851,-200 1851,-200 1851,-212 1851,-212 1851,-218 1845,-224 1839,-224"/> +<text text-anchor="middle" x="1744" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/connectivity</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/connectivity->context --> +<g id="edge226" class="edge"> +<title>google.golang.org/grpc/connectivity->context</title> +<path fill="none" stroke="#000000" d="M1851.3309,-190.917C1858.6628,-189.9195 1865.9391,-188.939 1873,-188 2089.4046,-159.2204 2350.0059,-127.1449 2440.8522,-116.04"/> +<polygon fill="#000000" stroke="#000000" points="2441.0704,-117.7764 2445.8212,-115.4328 2440.6459,-114.3022 2441.0704,-117.7764"/> +</g> +<!-- google.golang.org/grpc/grpclog --> +<g id="node66" class="node"> +<title>google.golang.org/grpc/grpclog</title> +<g id="a_node66"><a xlink:href="https://godoc.org/google.golang.org/grpc/grpclog" xlink:title="google.golang.org/grpc/grpclog" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1820,-130C1820,-130 1656,-130 1656,-130 1650,-130 1644,-124 1644,-118 1644,-118 1644,-106 1644,-106 1644,-100 1650,-94 1656,-94 1656,-94 1820,-94 1820,-94 1826,-94 1832,-100 1832,-106 1832,-106 1832,-118 1832,-118 1832,-124 1826,-130 1820,-130"/> +<text text-anchor="middle" x="1738" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">google.golang.org/grpc/grpclog</text> +</a> +</g> +</g> +<!-- google.golang.org/grpc/connectivity->google.golang.org/grpc/grpclog --> +<g id="edge227" class="edge"> +<title>google.golang.org/grpc/connectivity->google.golang.org/grpc/grpclog</title> +<path fill="none" stroke="#000000" d="M1742.8431,-187.8759C1741.8886,-172.9211 1740.5276,-151.5983 1739.4912,-135.3629"/> +<polygon fill="#000000" stroke="#000000" points="1741.2183,-134.9468 1739.1533,-130.0685 1737.7255,-135.1699 1741.2183,-134.9468"/> +</g> +<!-- google.golang.org/grpc/grpclog->io --> +<g id="edge228" class="edge"> +<title>google.golang.org/grpc/grpclog->io</title> +<path fill="none" stroke="#000000" d="M1643.6904,-97.8906C1634.0063,-96.5341 1624.3093,-95.2133 1615,-94 1390.0992,-64.6871 1331.9839,-71.6485 1108,-36 1089.4379,-33.0457 1068.8472,-28.8832 1052.2531,-25.305"/> +<polygon fill="#000000" stroke="#000000" points="1052.4957,-23.567 1047.2381,-24.2153 1051.7525,-26.9871 1052.4957,-23.567"/> +</g> +<!-- google.golang.org/grpc/grpclog->os --> +<g id="edge231" class="edge"> +<title>google.golang.org/grpc/grpclog->os</title> +<path fill="none" stroke="#000000" d="M1832.1241,-108.9128C2097.3981,-100.0108 2873.7408,-72.5747 3518,-36 3597.847,-31.4671 3691.6867,-24.3518 3739.8681,-20.5676"/> +<polygon fill="#000000" stroke="#000000" points="3740.103,-22.3046 3744.9502,-20.1675 3739.8282,-18.8154 3740.103,-22.3046"/> +</g> +<!-- google.golang.org/grpc/grpclog->strconv --> +<g id="edge232" class="edge"> +<title>google.golang.org/grpc/grpclog->strconv</title> +<path fill="none" stroke="#000000" d="M1643.6761,-102.6865C1436.8219,-82.2618 951.2631,-34.318 819.4957,-21.3073"/> +<polygon fill="#000000" stroke="#000000" points="819.3696,-19.5365 814.2219,-20.7866 819.0257,-23.0195 819.3696,-19.5365"/> +</g> +<!-- google.golang.org/grpc/grpclog->io/ioutil --> +<g id="edge229" class="edge"> +<title>google.golang.org/grpc/grpclog->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1643.9627,-106.6427C1329.5495,-88.7307 323.4217,-31.4119 122.9409,-19.9906"/> +<polygon fill="#000000" stroke="#000000" points="122.8905,-18.235 117.799,-19.6976 122.6914,-21.7293 122.8905,-18.235"/> +</g> +<!-- google.golang.org/grpc/grpclog->log --> +<g id="edge230" class="edge"> +<title>google.golang.org/grpc/grpclog->log</title> +<path fill="none" stroke="#000000" d="M1832.3194,-109.2693C2211.6247,-98.1887 3614.804,-56.2137 3813,-36 3838.33,-33.4166 3866.7405,-28.5434 3887.9173,-24.5185"/> +<polygon fill="#000000" stroke="#000000" points="3888.2853,-26.2299 3892.8657,-23.5685 3887.6254,-22.7927 3888.2853,-26.2299"/> +</g> +<!-- google.golang.org/grpc/internal->context --> +<g id="edge233" class="edge"> +<title>google.golang.org/grpc/internal->context</title> +<path fill="none" stroke="#000000" d="M1834.5225,-281.6535C1850.3382,-255.9583 1882.822,-209.9441 1924,-188 2014.6619,-139.6855 2335.6434,-119.1387 2440.553,-113.6141"/> +<polygon fill="#000000" stroke="#000000" points="2440.7772,-115.3549 2445.6793,-113.3471 2440.595,-111.8596 2440.7772,-115.3549"/> +</g> +<!-- google.golang.org/grpc/internal->time --> +<g id="edge235" class="edge"> +<title>google.golang.org/grpc/internal->time</title> +<path fill="none" stroke="#000000" d="M1763.931,-281.8814C1722.8146,-268.4122 1667.6849,-248.2197 1622,-224 1533.5338,-177.1 1528.7388,-136.3361 1438,-94 1350.3828,-53.1204 1237.1265,-31.4145 1182.4175,-22.6808"/> +<polygon fill="#000000" stroke="#000000" points="1182.5201,-20.9255 1177.3086,-21.8759 1181.9753,-24.3828 1182.5201,-20.9255"/> +</g> +<!-- google.golang.org/grpc/internal->google.golang.org/grpc/connectivity --> +<g id="edge234" class="edge"> +<title>google.golang.org/grpc/internal->google.golang.org/grpc/connectivity</title> +<path fill="none" stroke="#000000" d="M1808.5752,-281.8759C1795.5157,-266.531 1776.7502,-244.4815 1762.8109,-228.1029"/> +<polygon fill="#000000" stroke="#000000" points="1763.9508,-226.742 1759.3775,-224.0685 1761.2854,-229.0104 1763.9508,-226.742"/> +</g> +</g> +</svg> diff --git a/images/containers.dot.svg b/images/containers.dot.svg new file mode 100644 index 0000000..38135cf --- /dev/null +++ b/images/containers.dot.svg @@ -0,0 +1,5365 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: godep Pages: 1 --> +<svg width="4159pt" height="20510pt" + viewBox="0.00 0.00 4159.00 20509.79" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 20505.7905)"> +<title>godep</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-20505.7905 4155,-20505.7905 4155,4 -4,4"/> +<!-- bufio --> +<g id="node1" class="node"> +<title>bufio</title> +<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-13964.5998C4111,-13964.5998 4081,-13964.5998 4081,-13964.5998 4075,-13964.5998 4069,-13958.5998 4069,-13952.5998 4069,-13952.5998 4069,-13940.5998 4069,-13940.5998 4069,-13934.5998 4075,-13928.5998 4081,-13928.5998 4081,-13928.5998 4111,-13928.5998 4111,-13928.5998 4117,-13928.5998 4123,-13934.5998 4123,-13940.5998 4123,-13940.5998 4123,-13952.5998 4123,-13952.5998 4123,-13958.5998 4117,-13964.5998 4111,-13964.5998"/> +<text text-anchor="middle" x="4096" y="-13942.8998" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text> +</a> +</g> +</g> +<!-- bytes --> +<g id="node2" class="node"> +<title>bytes</title> +<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-13722.5998C4111,-13722.5998 4081,-13722.5998 4081,-13722.5998 4075,-13722.5998 4069,-13716.5998 4069,-13710.5998 4069,-13710.5998 4069,-13698.5998 4069,-13698.5998 4069,-13692.5998 4075,-13686.5998 4081,-13686.5998 4081,-13686.5998 4111,-13686.5998 4111,-13686.5998 4117,-13686.5998 4123,-13692.5998 4123,-13698.5998 4123,-13698.5998 4123,-13710.5998 4123,-13710.5998 4123,-13716.5998 4117,-13722.5998 4111,-13722.5998"/> +<text text-anchor="middle" x="4096" y="-13700.8998" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text> +</a> +</g> +</g> +<!-- compress/bzip2 --> +<g id="node3" class="node"> +<title>compress/bzip2</title> +<g id="a_node3"><a xlink:href="https://godoc.org/compress/bzip2" xlink:title="compress/bzip2" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2996,-12242.5998C2996,-12242.5998 2918,-12242.5998 2918,-12242.5998 2912,-12242.5998 2906,-12236.5998 2906,-12230.5998 2906,-12230.5998 2906,-12218.5998 2906,-12218.5998 2906,-12212.5998 2912,-12206.5998 2918,-12206.5998 2918,-12206.5998 2996,-12206.5998 2996,-12206.5998 3002,-12206.5998 3008,-12212.5998 3008,-12218.5998 3008,-12218.5998 3008,-12230.5998 3008,-12230.5998 3008,-12236.5998 3002,-12242.5998 2996,-12242.5998"/> +<text text-anchor="middle" x="2957" y="-12220.8998" font-family="Times,serif" font-size="14.00" fill="#000000">compress/bzip2</text> +</a> +</g> +</g> +<!-- compress/gzip --> +<g id="node4" class="node"> +<title>compress/gzip</title> +<g id="a_node4"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2612,-5544.5998C2612,-5544.5998 2541,-5544.5998 2541,-5544.5998 2535,-5544.5998 2529,-5538.5998 2529,-5532.5998 2529,-5532.5998 2529,-5520.5998 2529,-5520.5998 2529,-5514.5998 2535,-5508.5998 2541,-5508.5998 2541,-5508.5998 2612,-5508.5998 2612,-5508.5998 2618,-5508.5998 2624,-5514.5998 2624,-5520.5998 2624,-5520.5998 2624,-5532.5998 2624,-5532.5998 2624,-5538.5998 2618,-5544.5998 2612,-5544.5998"/> +<text text-anchor="middle" x="2576.5" y="-5522.8998" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text> +</a> +</g> +</g> +<!-- context --> +<g id="node5" class="node"> +<title>context</title> +<g id="a_node5"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4112,-952.5998C4112,-952.5998 4080,-952.5998 4080,-952.5998 4074,-952.5998 4068,-946.5998 4068,-940.5998 4068,-940.5998 4068,-928.5998 4068,-928.5998 4068,-922.5998 4074,-916.5998 4080,-916.5998 4080,-916.5998 4112,-916.5998 4112,-916.5998 4118,-916.5998 4124,-922.5998 4124,-928.5998 4124,-928.5998 4124,-940.5998 4124,-940.5998 4124,-946.5998 4118,-952.5998 4112,-952.5998"/> +<text text-anchor="middle" x="4096" y="-930.8998" font-family="Times,serif" font-size="14.00" fill="#000000">context</text> +</a> +</g> +</g> +<!-- crypto --> +<g id="node6" class="node"> +<title>crypto</title> +<g id="a_node6"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-16453.5998C4111,-16453.5998 4081,-16453.5998 4081,-16453.5998 4075,-16453.5998 4069,-16447.5998 4069,-16441.5998 4069,-16441.5998 4069,-16429.5998 4069,-16429.5998 4069,-16423.5998 4075,-16417.5998 4081,-16417.5998 4081,-16417.5998 4111,-16417.5998 4111,-16417.5998 4117,-16417.5998 4123,-16423.5998 4123,-16429.5998 4123,-16429.5998 4123,-16441.5998 4123,-16441.5998 4123,-16447.5998 4117,-16453.5998 4111,-16453.5998"/> +<text text-anchor="middle" x="4096" y="-16431.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text> +</a> +</g> +</g> +<!-- crypto/ecdsa --> +<g id="node7" class="node"> +<title>crypto/ecdsa</title> +<g id="a_node7"><a xlink:href="https://godoc.org/crypto/ecdsa" xlink:title="crypto/ecdsa" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-16670.5998C4126.5,-16670.5998 4065.5,-16670.5998 4065.5,-16670.5998 4059.5,-16670.5998 4053.5,-16664.5998 4053.5,-16658.5998 4053.5,-16658.5998 4053.5,-16646.5998 4053.5,-16646.5998 4053.5,-16640.5998 4059.5,-16634.5998 4065.5,-16634.5998 4065.5,-16634.5998 4126.5,-16634.5998 4126.5,-16634.5998 4132.5,-16634.5998 4138.5,-16640.5998 4138.5,-16646.5998 4138.5,-16646.5998 4138.5,-16658.5998 4138.5,-16658.5998 4138.5,-16664.5998 4132.5,-16670.5998 4126.5,-16670.5998"/> +<text text-anchor="middle" x="4096" y="-16648.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/ecdsa</text> +</a> +</g> +</g> +<!-- crypto/elliptic --> +<g id="node8" class="node"> +<title>crypto/elliptic</title> +<g id="a_node8"><a xlink:href="https://godoc.org/crypto/elliptic" xlink:title="crypto/elliptic" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4130,-17125.5998C4130,-17125.5998 4062,-17125.5998 4062,-17125.5998 4056,-17125.5998 4050,-17119.5998 4050,-17113.5998 4050,-17113.5998 4050,-17101.5998 4050,-17101.5998 4050,-17095.5998 4056,-17089.5998 4062,-17089.5998 4062,-17089.5998 4130,-17089.5998 4130,-17089.5998 4136,-17089.5998 4142,-17095.5998 4142,-17101.5998 4142,-17101.5998 4142,-17113.5998 4142,-17113.5998 4142,-17119.5998 4136,-17125.5998 4130,-17125.5998"/> +<text text-anchor="middle" x="4096" y="-17103.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/elliptic</text> +</a> +</g> +</g> +<!-- crypto/rand --> +<g id="node9" class="node"> +<title>crypto/rand</title> +<g id="a_node9"><a xlink:href="https://godoc.org/crypto/rand" xlink:title="crypto/rand" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4123.5,-19192.5998C4123.5,-19192.5998 4068.5,-19192.5998 4068.5,-19192.5998 4062.5,-19192.5998 4056.5,-19186.5998 4056.5,-19180.5998 4056.5,-19180.5998 4056.5,-19168.5998 4056.5,-19168.5998 4056.5,-19162.5998 4062.5,-19156.5998 4068.5,-19156.5998 4068.5,-19156.5998 4123.5,-19156.5998 4123.5,-19156.5998 4129.5,-19156.5998 4135.5,-19162.5998 4135.5,-19168.5998 4135.5,-19168.5998 4135.5,-19180.5998 4135.5,-19180.5998 4135.5,-19186.5998 4129.5,-19192.5998 4123.5,-19192.5998"/> +<text text-anchor="middle" x="4096" y="-19170.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/rand</text> +</a> +</g> +</g> +<!-- crypto/rsa --> +<g id="node10" class="node"> +<title>crypto/rsa</title> +<g id="a_node10"><a xlink:href="https://godoc.org/crypto/rsa" xlink:title="crypto/rsa" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4119.5,-17060.5998C4119.5,-17060.5998 4072.5,-17060.5998 4072.5,-17060.5998 4066.5,-17060.5998 4060.5,-17054.5998 4060.5,-17048.5998 4060.5,-17048.5998 4060.5,-17036.5998 4060.5,-17036.5998 4060.5,-17030.5998 4066.5,-17024.5998 4072.5,-17024.5998 4072.5,-17024.5998 4119.5,-17024.5998 4119.5,-17024.5998 4125.5,-17024.5998 4131.5,-17030.5998 4131.5,-17036.5998 4131.5,-17036.5998 4131.5,-17048.5998 4131.5,-17048.5998 4131.5,-17054.5998 4125.5,-17060.5998 4119.5,-17060.5998"/> +<text text-anchor="middle" x="4096" y="-17038.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/rsa</text> +</a> +</g> +</g> +<!-- crypto/sha256 --> +<g id="node11" class="node"> +<title>crypto/sha256</title> +<g id="a_node11"><a xlink:href="https://godoc.org/crypto/sha256" xlink:title="crypto/sha256" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4130.5,-16605.5998C4130.5,-16605.5998 4061.5,-16605.5998 4061.5,-16605.5998 4055.5,-16605.5998 4049.5,-16599.5998 4049.5,-16593.5998 4049.5,-16593.5998 4049.5,-16581.5998 4049.5,-16581.5998 4049.5,-16575.5998 4055.5,-16569.5998 4061.5,-16569.5998 4061.5,-16569.5998 4130.5,-16569.5998 4130.5,-16569.5998 4136.5,-16569.5998 4142.5,-16575.5998 4142.5,-16581.5998 4142.5,-16581.5998 4142.5,-16593.5998 4142.5,-16593.5998 4142.5,-16599.5998 4136.5,-16605.5998 4130.5,-16605.5998"/> +<text text-anchor="middle" x="4096" y="-16583.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/sha256</text> +</a> +</g> +</g> +<!-- crypto/sha512 --> +<g id="node12" class="node"> +<title>crypto/sha512</title> +<g id="a_node12"><a xlink:href="https://godoc.org/crypto/sha512" xlink:title="crypto/sha512" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4130.5,-16995.5998C4130.5,-16995.5998 4061.5,-16995.5998 4061.5,-16995.5998 4055.5,-16995.5998 4049.5,-16989.5998 4049.5,-16983.5998 4049.5,-16983.5998 4049.5,-16971.5998 4049.5,-16971.5998 4049.5,-16965.5998 4055.5,-16959.5998 4061.5,-16959.5998 4061.5,-16959.5998 4130.5,-16959.5998 4130.5,-16959.5998 4136.5,-16959.5998 4142.5,-16965.5998 4142.5,-16971.5998 4142.5,-16971.5998 4142.5,-16983.5998 4142.5,-16983.5998 4142.5,-16989.5998 4136.5,-16995.5998 4130.5,-16995.5998"/> +<text text-anchor="middle" x="4096" y="-16973.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/sha512</text> +</a> +</g> +</g> +<!-- crypto/tls --> +<g id="node13" class="node"> +<title>crypto/tls</title> +<g id="a_node13"><a xlink:href="https://godoc.org/crypto/tls" xlink:title="crypto/tls" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4118,-19280.5998C4118,-19280.5998 4074,-19280.5998 4074,-19280.5998 4068,-19280.5998 4062,-19274.5998 4062,-19268.5998 4062,-19268.5998 4062,-19256.5998 4062,-19256.5998 4062,-19250.5998 4068,-19244.5998 4074,-19244.5998 4074,-19244.5998 4118,-19244.5998 4118,-19244.5998 4124,-19244.5998 4130,-19250.5998 4130,-19256.5998 4130,-19256.5998 4130,-19268.5998 4130,-19268.5998 4130,-19274.5998 4124,-19280.5998 4118,-19280.5998"/> +<text text-anchor="middle" x="4096" y="-19258.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/tls</text> +</a> +</g> +</g> +<!-- crypto/x509 --> +<g id="node14" class="node"> +<title>crypto/x509</title> +<g id="a_node14"><a xlink:href="https://godoc.org/crypto/x509" xlink:title="crypto/x509" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4125,-18234.5998C4125,-18234.5998 4067,-18234.5998 4067,-18234.5998 4061,-18234.5998 4055,-18228.5998 4055,-18222.5998 4055,-18222.5998 4055,-18210.5998 4055,-18210.5998 4055,-18204.5998 4061,-18198.5998 4067,-18198.5998 4067,-18198.5998 4125,-18198.5998 4125,-18198.5998 4131,-18198.5998 4137,-18204.5998 4137,-18210.5998 4137,-18210.5998 4137,-18222.5998 4137,-18222.5998 4137,-18228.5998 4131,-18234.5998 4125,-18234.5998"/> +<text text-anchor="middle" x="4096" y="-18212.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/x509</text> +</a> +</g> +</g> +<!-- crypto/x509/pkix --> +<g id="node15" class="node"> +<title>crypto/x509/pkix</title> +<g id="a_node15"><a xlink:href="https://godoc.org/crypto/x509/pkix" xlink:title="crypto/x509/pkix" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4139,-16865.5998C4139,-16865.5998 4053,-16865.5998 4053,-16865.5998 4047,-16865.5998 4041,-16859.5998 4041,-16853.5998 4041,-16853.5998 4041,-16841.5998 4041,-16841.5998 4041,-16835.5998 4047,-16829.5998 4053,-16829.5998 4053,-16829.5998 4139,-16829.5998 4139,-16829.5998 4145,-16829.5998 4151,-16835.5998 4151,-16841.5998 4151,-16841.5998 4151,-16853.5998 4151,-16853.5998 4151,-16859.5998 4145,-16865.5998 4139,-16865.5998"/> +<text text-anchor="middle" x="4096" y="-16843.8998" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/x509/pkix</text> +</a> +</g> +</g> +<!-- encoding --> +<g id="node16" class="node"> +<title>encoding</title> +<g id="a_node16"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4117,-4764.5998C4117,-4764.5998 4075,-4764.5998 4075,-4764.5998 4069,-4764.5998 4063,-4758.5998 4063,-4752.5998 4063,-4752.5998 4063,-4740.5998 4063,-4740.5998 4063,-4734.5998 4069,-4728.5998 4075,-4728.5998 4075,-4728.5998 4117,-4728.5998 4117,-4728.5998 4123,-4728.5998 4129,-4734.5998 4129,-4740.5998 4129,-4740.5998 4129,-4752.5998 4129,-4752.5998 4129,-4758.5998 4123,-4764.5998 4117,-4764.5998"/> +<text text-anchor="middle" x="4096" y="-4742.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text> +</a> +</g> +</g> +<!-- encoding/base32 --> +<g id="node17" class="node"> +<title>encoding/base32</title> +<g id="a_node17"><a xlink:href="https://godoc.org/encoding/base32" xlink:title="encoding/base32" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4137.5,-16800.5998C4137.5,-16800.5998 4054.5,-16800.5998 4054.5,-16800.5998 4048.5,-16800.5998 4042.5,-16794.5998 4042.5,-16788.5998 4042.5,-16788.5998 4042.5,-16776.5998 4042.5,-16776.5998 4042.5,-16770.5998 4048.5,-16764.5998 4054.5,-16764.5998 4054.5,-16764.5998 4137.5,-16764.5998 4137.5,-16764.5998 4143.5,-16764.5998 4149.5,-16770.5998 4149.5,-16776.5998 4149.5,-16776.5998 4149.5,-16788.5998 4149.5,-16788.5998 4149.5,-16794.5998 4143.5,-16800.5998 4137.5,-16800.5998"/> +<text text-anchor="middle" x="4096" y="-16778.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base32</text> +</a> +</g> +</g> +<!-- encoding/base64 --> +<g id="node18" class="node"> +<title>encoding/base64</title> +<g id="a_node18"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4137.5,-16930.5998C4137.5,-16930.5998 4054.5,-16930.5998 4054.5,-16930.5998 4048.5,-16930.5998 4042.5,-16924.5998 4042.5,-16918.5998 4042.5,-16918.5998 4042.5,-16906.5998 4042.5,-16906.5998 4042.5,-16900.5998 4048.5,-16894.5998 4054.5,-16894.5998 4054.5,-16894.5998 4137.5,-16894.5998 4137.5,-16894.5998 4143.5,-16894.5998 4149.5,-16900.5998 4149.5,-16906.5998 4149.5,-16906.5998 4149.5,-16918.5998 4149.5,-16918.5998 4149.5,-16924.5998 4143.5,-16930.5998 4137.5,-16930.5998"/> +<text text-anchor="middle" x="4096" y="-16908.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text> +</a> +</g> +</g> +<!-- encoding/binary --> +<g id="node19" class="node"> +<title>encoding/binary</title> +<g id="a_node19"><a xlink:href="https://godoc.org/encoding/binary" xlink:title="encoding/binary" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4136,-12132.5998C4136,-12132.5998 4056,-12132.5998 4056,-12132.5998 4050,-12132.5998 4044,-12126.5998 4044,-12120.5998 4044,-12120.5998 4044,-12108.5998 4044,-12108.5998 4044,-12102.5998 4050,-12096.5998 4056,-12096.5998 4056,-12096.5998 4136,-12096.5998 4136,-12096.5998 4142,-12096.5998 4148,-12102.5998 4148,-12108.5998 4148,-12108.5998 4148,-12120.5998 4148,-12120.5998 4148,-12126.5998 4142,-12132.5998 4136,-12132.5998"/> +<text text-anchor="middle" x="4096" y="-12110.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/binary</text> +</a> +</g> +</g> +<!-- encoding/hex --> +<g id="node20" class="node"> +<title>encoding/hex</title> +<g id="a_node20"><a xlink:href="https://godoc.org/encoding/hex" xlink:title="encoding/hex" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3814,-9299.5998C3814,-9299.5998 3749,-9299.5998 3749,-9299.5998 3743,-9299.5998 3737,-9293.5998 3737,-9287.5998 3737,-9287.5998 3737,-9275.5998 3737,-9275.5998 3737,-9269.5998 3743,-9263.5998 3749,-9263.5998 3749,-9263.5998 3814,-9263.5998 3814,-9263.5998 3820,-9263.5998 3826,-9269.5998 3826,-9275.5998 3826,-9275.5998 3826,-9287.5998 3826,-9287.5998 3826,-9293.5998 3820,-9299.5998 3814,-9299.5998"/> +<text text-anchor="middle" x="3781.5" y="-9277.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/hex</text> +</a> +</g> +</g> +<!-- encoding/json --> +<g id="node21" class="node"> +<title>encoding/json</title> +<g id="a_node21"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4130,-17255.5998C4130,-17255.5998 4062,-17255.5998 4062,-17255.5998 4056,-17255.5998 4050,-17249.5998 4050,-17243.5998 4050,-17243.5998 4050,-17231.5998 4050,-17231.5998 4050,-17225.5998 4056,-17219.5998 4062,-17219.5998 4062,-17219.5998 4130,-17219.5998 4130,-17219.5998 4136,-17219.5998 4142,-17225.5998 4142,-17231.5998 4142,-17231.5998 4142,-17243.5998 4142,-17243.5998 4142,-17249.5998 4136,-17255.5998 4130,-17255.5998"/> +<text text-anchor="middle" x="4096" y="-17233.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text> +</a> +</g> +</g> +<!-- encoding/pem --> +<g id="node22" class="node"> +<title>encoding/pem</title> +<g id="a_node22"><a xlink:href="https://godoc.org/encoding/pem" xlink:title="encoding/pem" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4131,-18509.5998C4131,-18509.5998 4061,-18509.5998 4061,-18509.5998 4055,-18509.5998 4049,-18503.5998 4049,-18497.5998 4049,-18497.5998 4049,-18485.5998 4049,-18485.5998 4049,-18479.5998 4055,-18473.5998 4061,-18473.5998 4061,-18473.5998 4131,-18473.5998 4131,-18473.5998 4137,-18473.5998 4143,-18479.5998 4143,-18485.5998 4143,-18485.5998 4143,-18497.5998 4143,-18497.5998 4143,-18503.5998 4137,-18509.5998 4131,-18509.5998"/> +<text text-anchor="middle" x="4096" y="-18487.8998" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/pem</text> +</a> +</g> +</g> +<!-- errors --> +<g id="node23" class="node"> +<title>errors</title> +<g id="a_node23"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-6800.5998C4111,-6800.5998 4081,-6800.5998 4081,-6800.5998 4075,-6800.5998 4069,-6794.5998 4069,-6788.5998 4069,-6788.5998 4069,-6776.5998 4069,-6776.5998 4069,-6770.5998 4075,-6764.5998 4081,-6764.5998 4081,-6764.5998 4111,-6764.5998 4111,-6764.5998 4117,-6764.5998 4123,-6770.5998 4123,-6776.5998 4123,-6776.5998 4123,-6788.5998 4123,-6788.5998 4123,-6794.5998 4117,-6800.5998 4111,-6800.5998"/> +<text text-anchor="middle" x="4096" y="-6778.8998" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text> +</a> +</g> +</g> +<!-- expvar --> +<g id="node24" class="node"> +<title>expvar</title> +<g id="a_node24"><a xlink:href="https://godoc.org/expvar" xlink:title="expvar" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2972,-6835.5998C2972,-6835.5998 2942,-6835.5998 2942,-6835.5998 2936,-6835.5998 2930,-6829.5998 2930,-6823.5998 2930,-6823.5998 2930,-6811.5998 2930,-6811.5998 2930,-6805.5998 2936,-6799.5998 2942,-6799.5998 2942,-6799.5998 2972,-6799.5998 2972,-6799.5998 2978,-6799.5998 2984,-6805.5998 2984,-6811.5998 2984,-6811.5998 2984,-6823.5998 2984,-6823.5998 2984,-6829.5998 2978,-6835.5998 2972,-6835.5998"/> +<text text-anchor="middle" x="2957" y="-6813.8998" font-family="Times,serif" font-size="14.00" fill="#000000">expvar</text> +</a> +</g> +</g> +<!-- fmt --> +<g id="node25" class="node"> +<title>fmt</title> +<g id="a_node25"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-8759.5998C4111,-8759.5998 4081,-8759.5998 4081,-8759.5998 4075,-8759.5998 4069,-8753.5998 4069,-8747.5998 4069,-8747.5998 4069,-8735.5998 4069,-8735.5998 4069,-8729.5998 4075,-8723.5998 4081,-8723.5998 4081,-8723.5998 4111,-8723.5998 4111,-8723.5998 4117,-8723.5998 4123,-8729.5998 4123,-8735.5998 4123,-8735.5998 4123,-8747.5998 4123,-8747.5998 4123,-8753.5998 4117,-8759.5998 4111,-8759.5998"/> +<text text-anchor="middle" x="4096" y="-8737.8998" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml --> +<g id="node26" class="node"> +<title>github.com/BurntSushi/toml</title> +<g id="a_node26"><a xlink:href="https://godoc.org/github.com/BurntSushi/toml" xlink:title="github.com/BurntSushi/toml" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3856,-5068.5998C3856,-5068.5998 3707,-5068.5998 3707,-5068.5998 3701,-5068.5998 3695,-5062.5998 3695,-5056.5998 3695,-5056.5998 3695,-5044.5998 3695,-5044.5998 3695,-5038.5998 3701,-5032.5998 3707,-5032.5998 3707,-5032.5998 3856,-5032.5998 3856,-5032.5998 3862,-5032.5998 3868,-5038.5998 3868,-5044.5998 3868,-5044.5998 3868,-5056.5998 3868,-5056.5998 3868,-5062.5998 3862,-5068.5998 3856,-5068.5998"/> +<text text-anchor="middle" x="3781.5" y="-5046.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/BurntSushi/toml</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->bufio --> +<g id="edge1" class="edge"> +<title>github.com/BurntSushi/toml->bufio</title> +<path fill="none" stroke="#000000" d="M3801.8355,-5068.6028C3845.2465,-5108.5584 3946.0347,-5209.8307 3983,-5319.5998 4059.1976,-5545.8698 3921.2706,-13708.035 4041,-13914.5998 4046.2845,-13923.717 4055.2673,-13930.4827 4064.4585,-13935.3836"/> +<polygon fill="#000000" stroke="#000000" points="4063.7174,-13936.9695 4068.972,-13937.6417 4065.2835,-13933.8394 4063.7174,-13936.9695"/> +</g> +<!-- github.com/BurntSushi/toml->encoding --> +<g id="edge2" class="edge"> +<title>github.com/BurntSushi/toml->encoding</title> +<path fill="none" stroke="#000000" d="M3848.6679,-5032.5935C3891.2434,-5018.4258 3945.2123,-4995.0587 3983,-4959.5998 4041.52,-4904.6864 4075.7626,-4812.5703 4089.2712,-4769.765"/> +<polygon fill="#000000" stroke="#000000" points="4091.003,-4770.0892 4090.8161,-4764.7951 4087.6607,-4769.0502 4091.003,-4770.0892"/> +</g> +<!-- github.com/BurntSushi/toml->errors --> +<g id="edge3" class="edge"> +<title>github.com/BurntSushi/toml->errors</title> +<path fill="none" stroke="#000000" d="M3868.0246,-5045.2629C3908.9066,-5047.6916 3954.8139,-5058.5122 3983,-5090.5998 4040.7474,-5156.3404 4088.7051,-6559.6055 4095.2413,-6759.0902"/> +<polygon fill="#000000" stroke="#000000" points="4093.5016,-6759.4353 4095.414,-6764.3755 4096.9997,-6759.321 4093.5016,-6759.4353"/> +</g> +<!-- github.com/BurntSushi/toml->fmt --> +<g id="edge4" class="edge"> +<title>github.com/BurntSushi/toml->fmt</title> +<path fill="none" stroke="#000000" d="M3801.7125,-5068.6447C3844.8814,-5108.6829 3945.222,-5210.1077 3983,-5319.5998 4031.7181,-5460.7996 4033.8058,-7858.4051 4041,-8007.5998 4054.5175,-8287.9281 4085.322,-8627.4946 4093.803,-8718.3329"/> +<polygon fill="#000000" stroke="#000000" points="4092.0747,-8718.6477 4094.283,-8723.4629 4095.5595,-8718.3216 4092.0747,-8718.6477"/> +</g> +<!-- io --> +<g id="node27" class="node"> +<title>io</title> +<g id="a_node27"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-13899.5998C4111,-13899.5998 4081,-13899.5998 4081,-13899.5998 4075,-13899.5998 4069,-13893.5998 4069,-13887.5998 4069,-13887.5998 4069,-13875.5998 4069,-13875.5998 4069,-13869.5998 4075,-13863.5998 4081,-13863.5998 4081,-13863.5998 4111,-13863.5998 4111,-13863.5998 4117,-13863.5998 4123,-13869.5998 4123,-13875.5998 4123,-13875.5998 4123,-13887.5998 4123,-13887.5998 4123,-13893.5998 4117,-13899.5998 4111,-13899.5998"/> +<text text-anchor="middle" x="4096" y="-13877.8998" font-family="Times,serif" font-size="14.00" fill="#000000">io</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->io --> +<g id="edge5" class="edge"> +<title>github.com/BurntSushi/toml->io</title> +<path fill="none" stroke="#000000" d="M3801.8343,-5068.6032C3845.243,-5108.5596 3946.0269,-5209.8333 3983,-5319.5998 4057.6442,-5541.205 3997.7868,-13507.7885 4041,-13737.5998 4049.4151,-13782.3518 4070.9421,-13831.2307 4084.433,-13858.9794"/> +<polygon fill="#000000" stroke="#000000" points="4082.9159,-13859.8605 4086.6896,-13863.5783 4086.058,-13858.3186 4082.9159,-13859.8605"/> +</g> +<!-- io/ioutil --> +<g id="node28" class="node"> +<title>io/ioutil</title> +<g id="a_node28"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-16318.5998C4113.5,-16318.5998 4078.5,-16318.5998 4078.5,-16318.5998 4072.5,-16318.5998 4066.5,-16312.5998 4066.5,-16306.5998 4066.5,-16306.5998 4066.5,-16294.5998 4066.5,-16294.5998 4066.5,-16288.5998 4072.5,-16282.5998 4078.5,-16282.5998 4078.5,-16282.5998 4113.5,-16282.5998 4113.5,-16282.5998 4119.5,-16282.5998 4125.5,-16288.5998 4125.5,-16294.5998 4125.5,-16294.5998 4125.5,-16306.5998 4125.5,-16306.5998 4125.5,-16312.5998 4119.5,-16318.5998 4113.5,-16318.5998"/> +<text text-anchor="middle" x="4096" y="-16296.8998" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->io/ioutil --> +<g id="edge6" class="edge"> +<title>github.com/BurntSushi/toml->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3801.8363,-5068.6025C3845.249,-5108.5576 3946.0402,-5209.8288 3983,-5319.5998 4021.6692,-5434.4478 4039.1204,-13923.4312 4041,-14044.5998 4055.3143,-14967.3713 4089.8535,-16102.0781 4095.2778,-16277.3974"/> +<polygon fill="#000000" stroke="#000000" points="4093.533,-16277.5928 4095.437,-16282.5362 4097.0313,-16277.4844 4093.533,-16277.5928"/> +</g> +<!-- math --> +<g id="node29" class="node"> +<title>math</title> +<g id="a_node29"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-3861.5998C4111,-3861.5998 4081,-3861.5998 4081,-3861.5998 4075,-3861.5998 4069,-3855.5998 4069,-3849.5998 4069,-3849.5998 4069,-3837.5998 4069,-3837.5998 4069,-3831.5998 4075,-3825.5998 4081,-3825.5998 4081,-3825.5998 4111,-3825.5998 4111,-3825.5998 4117,-3825.5998 4123,-3831.5998 4123,-3837.5998 4123,-3837.5998 4123,-3849.5998 4123,-3849.5998 4123,-3855.5998 4117,-3861.5998 4111,-3861.5998"/> +<text text-anchor="middle" x="4096" y="-3839.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->math --> +<g id="edge7" class="edge"> +<title>github.com/BurntSushi/toml->math</title> +<path fill="none" stroke="#000000" d="M3868.1396,-5033.2736C3909.8421,-5020.2512 3956.4446,-4997.8217 3983,-4959.5998 4037.266,-4881.4933 4027.9766,-4199.8114 4041,-4105.5998 4053.3176,-4016.494 4078.4466,-3912.3695 4089.9754,-3866.8649"/> +<polygon fill="#000000" stroke="#000000" points="4091.7095,-3867.1463 4091.2462,-3861.8692 4088.3175,-3866.2834 4091.7095,-3867.1463"/> +</g> +<!-- reflect --> +<g id="node30" class="node"> +<title>reflect</title> +<g id="a_node30"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-5037.5998C4111,-5037.5998 4081,-5037.5998 4081,-5037.5998 4075,-5037.5998 4069,-5031.5998 4069,-5025.5998 4069,-5025.5998 4069,-5013.5998 4069,-5013.5998 4069,-5007.5998 4075,-5001.5998 4081,-5001.5998 4081,-5001.5998 4111,-5001.5998 4111,-5001.5998 4117,-5001.5998 4123,-5007.5998 4123,-5013.5998 4123,-5013.5998 4123,-5025.5998 4123,-5025.5998 4123,-5031.5998 4117,-5037.5998 4111,-5037.5998"/> +<text text-anchor="middle" x="4096" y="-5015.8998" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->reflect --> +<g id="edge8" class="edge"> +<title>github.com/BurntSushi/toml->reflect</title> +<path fill="none" stroke="#000000" d="M3868.271,-5042.0469C3933.0468,-5035.662 4017.9401,-5027.2941 4063.5899,-5022.7945"/> +<polygon fill="#000000" stroke="#000000" points="4063.8854,-5024.5239 4068.6896,-5022.2918 4063.542,-5021.0408 4063.8854,-5024.5239"/> +</g> +<!-- sort --> +<g id="node31" class="node"> +<title>sort</title> +<g id="a_node31"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-4460.5998C4111,-4460.5998 4081,-4460.5998 4081,-4460.5998 4075,-4460.5998 4069,-4454.5998 4069,-4448.5998 4069,-4448.5998 4069,-4436.5998 4069,-4436.5998 4069,-4430.5998 4075,-4424.5998 4081,-4424.5998 4081,-4424.5998 4111,-4424.5998 4111,-4424.5998 4117,-4424.5998 4123,-4430.5998 4123,-4436.5998 4123,-4436.5998 4123,-4448.5998 4123,-4448.5998 4123,-4454.5998 4117,-4460.5998 4111,-4460.5998"/> +<text text-anchor="middle" x="4096" y="-4438.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->sort --> +<g id="edge9" class="edge"> +<title>github.com/BurntSushi/toml->sort</title> +<path fill="none" stroke="#000000" d="M3862.821,-5032.5467C3904.6318,-5019.1799 3952.9779,-4996.6577 3983,-4959.5998 3991.0611,-4949.6496 4070.83,-4564.6991 4091.2555,-4465.6487"/> +<polygon fill="#000000" stroke="#000000" points="4092.9985,-4465.8606 4092.2942,-4460.6102 4089.5706,-4465.1539 4092.9985,-4465.8606"/> +</g> +<!-- strconv --> +<g id="node32" class="node"> +<title>strconv</title> +<g id="a_node32"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4112,-5369.5998C4112,-5369.5998 4080,-5369.5998 4080,-5369.5998 4074,-5369.5998 4068,-5363.5998 4068,-5357.5998 4068,-5357.5998 4068,-5345.5998 4068,-5345.5998 4068,-5339.5998 4074,-5333.5998 4080,-5333.5998 4080,-5333.5998 4112,-5333.5998 4112,-5333.5998 4118,-5333.5998 4124,-5339.5998 4124,-5345.5998 4124,-5345.5998 4124,-5357.5998 4124,-5357.5998 4124,-5363.5998 4118,-5369.5998 4112,-5369.5998"/> +<text text-anchor="middle" x="4096" y="-5347.8998" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->strconv --> +<g id="edge10" class="edge"> +<title>github.com/BurntSushi/toml->strconv</title> +<path fill="none" stroke="#000000" d="M3868.2478,-5050.1661C3906.9897,-5054.1043 3951.0149,-5064.783 3983,-5090.5998 4059.7081,-5152.515 4085.4975,-5276.5946 4093.1347,-5328.2243"/> +<polygon fill="#000000" stroke="#000000" points="4091.4178,-5328.5804 4093.8587,-5333.282 4094.8825,-5328.0844 4091.4178,-5328.5804"/> +</g> +<!-- strings --> +<g id="node33" class="node"> +<title>strings</title> +<g id="a_node33"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-7095.5998C4111,-7095.5998 4081,-7095.5998 4081,-7095.5998 4075,-7095.5998 4069,-7089.5998 4069,-7083.5998 4069,-7083.5998 4069,-7071.5998 4069,-7071.5998 4069,-7065.5998 4075,-7059.5998 4081,-7059.5998 4081,-7059.5998 4111,-7059.5998 4111,-7059.5998 4117,-7059.5998 4123,-7065.5998 4123,-7071.5998 4123,-7071.5998 4123,-7083.5998 4123,-7083.5998 4123,-7089.5998 4117,-7095.5998 4111,-7095.5998"/> +<text text-anchor="middle" x="4096" y="-7073.8998" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->strings --> +<g id="edge11" class="edge"> +<title>github.com/BurntSushi/toml->strings</title> +<path fill="none" stroke="#000000" d="M3801.5704,-5068.6945C3844.4596,-5108.8304 3944.283,-5210.4362 3983,-5319.5998 4094.209,-5633.1568 4000.2362,-6485.4124 4041,-6815.5998 4052.0216,-6904.8752 4077.8142,-7008.9129 4089.7439,-7054.365"/> +<polygon fill="#000000" stroke="#000000" points="4088.0926,-7054.9664 4091.0598,-7059.3548 4091.4769,-7054.0738 4088.0926,-7054.9664"/> +</g> +<!-- sync --> +<g id="node34" class="node"> +<title>sync</title> +<g id="a_node34"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-3206.5998C4111,-3206.5998 4081,-3206.5998 4081,-3206.5998 4075,-3206.5998 4069,-3200.5998 4069,-3194.5998 4069,-3194.5998 4069,-3182.5998 4069,-3182.5998 4069,-3176.5998 4075,-3170.5998 4081,-3170.5998 4081,-3170.5998 4111,-3170.5998 4111,-3170.5998 4117,-3170.5998 4123,-3176.5998 4123,-3182.5998 4123,-3182.5998 4123,-3194.5998 4123,-3194.5998 4123,-3200.5998 4117,-3206.5998 4111,-3206.5998"/> +<text text-anchor="middle" x="4096" y="-3184.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->sync --> +<g id="edge12" class="edge"> +<title>github.com/BurntSushi/toml->sync</title> +<path fill="none" stroke="#000000" d="M3868.1574,-5033.5438C3909.9997,-5020.5844 3956.7228,-4998.1297 3983,-4959.5998 4054.9613,-4854.0843 4032.1604,-3939.0118 4041,-3811.5998 4057.2698,-3577.0907 4085.3567,-3293.6614 4093.6375,-3211.7734"/> +<polygon fill="#000000" stroke="#000000" points="4095.3912,-3211.8246 4094.154,-3206.6737 4091.909,-3211.4719 4095.3912,-3211.8246"/> +</g> +<!-- time --> +<g id="node35" class="node"> +<title>time</title> +<g id="a_node35"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-11403.5998C4111,-11403.5998 4081,-11403.5998 4081,-11403.5998 4075,-11403.5998 4069,-11397.5998 4069,-11391.5998 4069,-11391.5998 4069,-11379.5998 4069,-11379.5998 4069,-11373.5998 4075,-11367.5998 4081,-11367.5998 4081,-11367.5998 4111,-11367.5998 4111,-11367.5998 4117,-11367.5998 4123,-11373.5998 4123,-11379.5998 4123,-11379.5998 4123,-11391.5998 4123,-11391.5998 4123,-11397.5998 4117,-11403.5998 4111,-11403.5998"/> +<text text-anchor="middle" x="4096" y="-11381.8998" font-family="Times,serif" font-size="14.00" fill="#000000">time</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->time --> +<g id="edge13" class="edge"> +<title>github.com/BurntSushi/toml->time</title> +<path fill="none" stroke="#000000" d="M3801.8117,-5068.6108C3845.1759,-5108.5823 3945.8775,-5209.8837 3983,-5319.5998 4036.7221,-5478.3764 3956.7361,-11208.701 4041,-11353.5998 4046.2976,-11362.7094 4055.2837,-11369.4732 4064.4739,-11374.3746"/> +<polygon fill="#000000" stroke="#000000" points="4063.7321,-11375.9603 4068.9866,-11376.6333 4065.2987,-11372.8304 4063.7321,-11375.9603"/> +</g> +<!-- unicode --> +<g id="node36" class="node"> +<title>unicode</title> +<g id="a_node36"><a xlink:href="https://godoc.org/unicode" xlink:title="unicode" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-4155.5998C4113.5,-4155.5998 4078.5,-4155.5998 4078.5,-4155.5998 4072.5,-4155.5998 4066.5,-4149.5998 4066.5,-4143.5998 4066.5,-4143.5998 4066.5,-4131.5998 4066.5,-4131.5998 4066.5,-4125.5998 4072.5,-4119.5998 4078.5,-4119.5998 4078.5,-4119.5998 4113.5,-4119.5998 4113.5,-4119.5998 4119.5,-4119.5998 4125.5,-4125.5998 4125.5,-4131.5998 4125.5,-4131.5998 4125.5,-4143.5998 4125.5,-4143.5998 4125.5,-4149.5998 4119.5,-4155.5998 4113.5,-4155.5998"/> +<text text-anchor="middle" x="4096" y="-4133.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unicode</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->unicode --> +<g id="edge14" class="edge"> +<title>github.com/BurntSushi/toml->unicode</title> +<path fill="none" stroke="#000000" d="M3868.2374,-5032.8172C3909.6983,-5019.694 3956.0545,-4997.3121 3983,-4959.5998 4062.6757,-4848.0875 4016.9567,-4480.5262 4041,-4345.5998 4053.1662,-4277.3257 4076.6146,-4198.6777 4088.5812,-4160.6146"/> +<polygon fill="#000000" stroke="#000000" points="4090.2843,-4161.0327 4090.1215,-4155.7377 4086.9468,-4159.9785 4090.2843,-4161.0327"/> +</g> +<!-- unicode/utf8 --> +<g id="node37" class="node"> +<title>unicode/utf8</title> +<g id="a_node37"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-4395.5998C4126.5,-4395.5998 4065.5,-4395.5998 4065.5,-4395.5998 4059.5,-4395.5998 4053.5,-4389.5998 4053.5,-4383.5998 4053.5,-4383.5998 4053.5,-4371.5998 4053.5,-4371.5998 4053.5,-4365.5998 4059.5,-4359.5998 4065.5,-4359.5998 4065.5,-4359.5998 4126.5,-4359.5998 4126.5,-4359.5998 4132.5,-4359.5998 4138.5,-4365.5998 4138.5,-4371.5998 4138.5,-4371.5998 4138.5,-4383.5998 4138.5,-4383.5998 4138.5,-4389.5998 4132.5,-4395.5998 4126.5,-4395.5998"/> +<text text-anchor="middle" x="4096" y="-4373.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text> +</a> +</g> +</g> +<!-- github.com/BurntSushi/toml->unicode/utf8 --> +<g id="edge15" class="edge"> +<title>github.com/BurntSushi/toml->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3868.0629,-5032.6918C3909.4781,-5019.5357 3955.8461,-4997.1623 3983,-4959.5998 4126.743,-4760.7571 3909.0484,-4617.4555 4041,-4410.5998 4043.7847,-4406.2344 4047.3724,-4402.366 4051.3795,-4398.9582"/> +<polygon fill="#000000" stroke="#000000" points="4052.6675,-4400.1714 4055.5122,-4395.7026 4050.5016,-4397.422 4052.6675,-4400.1714"/> +</g> +<!-- github.com/beorn7/perks/quantile --> +<g id="node38" class="node"> +<title>github.com/beorn7/perks/quantile</title> +<g id="a_node38"><a xlink:href="https://godoc.org/github.com/beorn7/perks/quantile" xlink:title="github.com/beorn7/perks/quantile" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3869.5,-3622.5998C3869.5,-3622.5998 3693.5,-3622.5998 3693.5,-3622.5998 3687.5,-3622.5998 3681.5,-3616.5998 3681.5,-3610.5998 3681.5,-3610.5998 3681.5,-3598.5998 3681.5,-3598.5998 3681.5,-3592.5998 3687.5,-3586.5998 3693.5,-3586.5998 3693.5,-3586.5998 3869.5,-3586.5998 3869.5,-3586.5998 3875.5,-3586.5998 3881.5,-3592.5998 3881.5,-3598.5998 3881.5,-3598.5998 3881.5,-3610.5998 3881.5,-3610.5998 3881.5,-3616.5998 3875.5,-3622.5998 3869.5,-3622.5998"/> +<text text-anchor="middle" x="3781.5" y="-3600.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/beorn7/perks/quantile</text> +</a> +</g> +</g> +<!-- github.com/beorn7/perks/quantile->math --> +<g id="edge16" class="edge"> +<title>github.com/beorn7/perks/quantile->math</title> +<path fill="none" stroke="#000000" d="M3881.5317,-3607.6496C3916.2652,-3612.8458 3953.791,-3623.5188 3983,-3644.5998 4044.76,-3689.1739 4077.39,-3778.585 4089.8843,-3820.6838"/> +<polygon fill="#000000" stroke="#000000" points="4088.2309,-3821.2655 4091.3088,-3825.5771 4091.5914,-3820.2872 4088.2309,-3821.2655"/> +</g> +<!-- github.com/beorn7/perks/quantile->sort --> +<g id="edge17" class="edge"> +<title>github.com/beorn7/perks/quantile->sort</title> +<path fill="none" stroke="#000000" d="M3820.5599,-3622.7671C3868.0913,-3647.1815 3946.5072,-3695.4081 3983,-3762.5998 4121.0022,-4016.6941 3885.8028,-4166.6278 4041,-4410.5998 4046.4532,-4419.1723 4055.0867,-4425.7154 4063.9116,-4430.5809"/> +<polygon fill="#000000" stroke="#000000" points="4063.5695,-4432.3738 4068.815,-4433.1135 4065.1757,-4429.2641 4063.5695,-4432.3738"/> +</g> +<!-- github.com/cespare/xxhash/v2 --> +<g id="node39" class="node"> +<title>github.com/cespare/xxhash/v2</title> +<g id="a_node39"><a xlink:href="https://godoc.org/github.com/cespare/xxhash/v2" xlink:title="github.com/cespare/xxhash/v2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3860.5,-10739.5998C3860.5,-10739.5998 3702.5,-10739.5998 3702.5,-10739.5998 3696.5,-10739.5998 3690.5,-10733.5998 3690.5,-10727.5998 3690.5,-10727.5998 3690.5,-10715.5998 3690.5,-10715.5998 3690.5,-10709.5998 3696.5,-10703.5998 3702.5,-10703.5998 3702.5,-10703.5998 3860.5,-10703.5998 3860.5,-10703.5998 3866.5,-10703.5998 3872.5,-10709.5998 3872.5,-10715.5998 3872.5,-10715.5998 3872.5,-10727.5998 3872.5,-10727.5998 3872.5,-10733.5998 3866.5,-10739.5998 3860.5,-10739.5998"/> +<text text-anchor="middle" x="3781.5" y="-10717.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/cespare/xxhash/v2</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->encoding/binary --> +<g id="edge18" class="edge"> +<title>github.com/cespare/xxhash/v2->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3801.3418,-10739.7773C3843.7811,-10780.0764 3942.7726,-10881.9837 3983,-10990.5998 4050.3723,-11172.5087 4013.8778,-11669.521 4041,-11861.5998 4053.1029,-11947.3125 4078.1209,-12047.2398 4089.7741,-12091.4915"/> +<polygon fill="#000000" stroke="#000000" points="4088.0902,-12091.9693 4091.0603,-12096.3557 4091.4739,-12091.0745 4088.0902,-12091.9693"/> +</g> +<!-- github.com/cespare/xxhash/v2->errors --> +<g id="edge19" class="edge"> +<title>github.com/cespare/xxhash/v2->errors</title> +<path fill="none" stroke="#000000" d="M3825.1474,-10703.5215C3873.6984,-10680.7615 3949.6787,-10636.6064 3983,-10571.5998 4072.3666,-10397.254 4019.2394,-7240.303 4041,-7045.5998 4051.0278,-6955.8765 4077.3293,-6851.4966 4089.5664,-6805.9042"/> +<polygon fill="#000000" stroke="#000000" points="4091.3039,-6806.1823 4090.9169,-6800.899 4087.9248,-6805.2705 4091.3039,-6806.1823"/> +</g> +<!-- github.com/cespare/xxhash/v2->reflect --> +<g id="edge21" class="edge"> +<title>github.com/cespare/xxhash/v2->reflect</title> +<path fill="none" stroke="#000000" d="M3825.2105,-10703.5537C3873.82,-10680.8236 3949.8545,-10636.6963 3983,-10571.5998 4049.2,-10441.5855 4026.989,-5464.8233 4041,-5319.5998 4051.0783,-5215.1387 4078.5171,-5092.6113 4090.395,-5042.6026"/> +<polygon fill="#000000" stroke="#000000" points="4092.104,-5042.9801 4091.5624,-5037.7104 4088.6995,-5042.1676 4092.104,-5042.9801"/> +</g> +<!-- math/bits --> +<g id="node40" class="node"> +<title>math/bits</title> +<g id="a_node40"><a xlink:href="https://godoc.org/math/bits" xlink:title="math/bits" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4118,-10679.5998C4118,-10679.5998 4074,-10679.5998 4074,-10679.5998 4068,-10679.5998 4062,-10673.5998 4062,-10667.5998 4062,-10667.5998 4062,-10655.5998 4062,-10655.5998 4062,-10649.5998 4068,-10643.5998 4074,-10643.5998 4074,-10643.5998 4118,-10643.5998 4118,-10643.5998 4124,-10643.5998 4130,-10649.5998 4130,-10655.5998 4130,-10655.5998 4130,-10667.5998 4130,-10667.5998 4130,-10673.5998 4124,-10679.5998 4118,-10679.5998"/> +<text text-anchor="middle" x="4096" y="-10657.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math/bits</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->math/bits --> +<g id="edge20" class="edge"> +<title>github.com/cespare/xxhash/v2->math/bits</title> +<path fill="none" stroke="#000000" d="M3872.8889,-10704.1647C3933.8351,-10692.5375 4010.8917,-10677.8367 4056.6557,-10669.1059"/> +<polygon fill="#000000" stroke="#000000" points="4057.2213,-10670.7796 4061.8047,-10668.1236 4056.5653,-10667.3416 4057.2213,-10670.7796"/> +</g> +<!-- unsafe --> +<g id="node41" class="node"> +<title>unsafe</title> +<g id="a_node41"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-14029.5998C4111,-14029.5998 4081,-14029.5998 4081,-14029.5998 4075,-14029.5998 4069,-14023.5998 4069,-14017.5998 4069,-14017.5998 4069,-14005.5998 4069,-14005.5998 4069,-13999.5998 4075,-13993.5998 4081,-13993.5998 4081,-13993.5998 4111,-13993.5998 4111,-13993.5998 4117,-13993.5998 4123,-13999.5998 4123,-14005.5998 4123,-14005.5998 4123,-14017.5998 4123,-14017.5998 4123,-14023.5998 4117,-14029.5998 4111,-14029.5998"/> +<text text-anchor="middle" x="4096" y="-14007.8998" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->unsafe --> +<g id="edge22" class="edge"> +<title>github.com/cespare/xxhash/v2->unsafe</title> +<path fill="none" stroke="#000000" d="M3801.7305,-10739.6385C3844.9349,-10779.6645 3945.3409,-10881.0668 3983,-10990.5998 4037.0006,-11147.6628 3956.8045,-13836.4358 4041,-13979.5998 4046.294,-13988.6015 4055.1777,-13995.3186 4064.2783,-14000.2109"/> +<polygon fill="#000000" stroke="#000000" points="4063.4963,-14001.7765 4068.7484,-14002.468 4065.0739,-13998.6521 4063.4963,-14001.7765"/> +</g> +<!-- github.com/containers/image/docker --> +<g id="node42" class="node"> +<title>github.com/containers/image/docker</title> +<g id="a_node42"><a xlink:href="https://godoc.org/github.com/containers/image/docker" xlink:title="github.com/containers/image/docker" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M204,-9015.5998C204,-9015.5998 12,-9015.5998 12,-9015.5998 6,-9015.5998 0,-9009.5998 0,-9003.5998 0,-9003.5998 0,-8991.5998 0,-8991.5998 0,-8985.5998 6,-8979.5998 12,-8979.5998 12,-8979.5998 204,-8979.5998 204,-8979.5998 210,-8979.5998 216,-8985.5998 216,-8991.5998 216,-8991.5998 216,-9003.5998 216,-9003.5998 216,-9009.5998 210,-9015.5998 204,-9015.5998"/> +<text text-anchor="middle" x="108" y="-8993.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/docker</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->bytes --> +<g id="edge23" class="edge"> +<title>github.com/containers/image/docker->bytes</title> +<path fill="none" stroke="#000000" d="M108.2994,-9015.6083C112.2985,-9251.1292 156.8864,-11729.3631 274,-12451.5998 405.9589,-13265.3875 -49.4171,-14237.5998 775,-14237.5998 775,-14237.5998 775,-14237.5998 1848.5,-14237.5998 2295.8667,-14237.5998 2352.5907,-14491.8866 2785,-14606.5998 3102.5829,-14690.8509 3193.4348,-14666.0252 3522,-14664.5998 3726.8947,-14663.7109 3833.6448,-14800.8697 3983,-14660.5998 4048.8524,-14598.7532 4021.5521,-13937.823 4041,-13849.5998 4050.8621,-13804.8615 4071.9125,-13755.5363 4084.9211,-13727.4868"/> +<polygon fill="#000000" stroke="#000000" points="4086.5625,-13728.1079 4087.0943,-13722.8373 4083.3917,-13726.6259 4086.5625,-13728.1079"/> +</g> +<!-- github.com/containers/image/docker->context --> +<g id="edge24" class="edge"> +<title>github.com/containers/image/docker->context</title> +<path fill="none" stroke="#000000" d="M108.1721,-8979.5338C112.2011,-8558.9319 186.4301,-933.1535 274,-727.5998 397.4197,-437.8957 460.1018,-226.5998 775,-226.5998 775,-226.5998 775,-226.5998 3354.5,-226.5998 3661.9493,-226.5998 3784.5568,-280.7693 3983,-515.5998 4035.3575,-577.5578 4079.5844,-831.7349 4092.3149,-910.9282"/> +<polygon fill="#000000" stroke="#000000" points="4090.6597,-911.6613 4093.1767,-916.3226 4094.1159,-911.1091 4090.6597,-911.6613"/> +</g> +<!-- github.com/containers/image/docker->crypto/rand --> +<g id="edge25" class="edge"> +<title>github.com/containers/image/docker->crypto/rand</title> +<path fill="none" stroke="#000000" d="M108.1297,-9015.806C111.5759,-9496.8615 183.2608,-19352.2153 274,-19625.5998 378.9915,-19941.9244 441.7067,-20183.5998 775,-20183.5998 775,-20183.5998 775,-20183.5998 3354.5,-20183.5998 3641.0713,-20183.5998 3796.5144,-20257.1917 3983,-20039.5998 4100.2903,-19902.7453 3966.8511,-19394.8804 4041,-19230.5998 4046.9915,-19217.3254 4057.5663,-19205.3743 4067.8532,-19195.9553"/> +<polygon fill="#000000" stroke="#000000" points="4069.0495,-19197.2334 4071.6211,-19192.6019 4066.7226,-19194.6188 4069.0495,-19197.2334"/> +</g> +<!-- github.com/containers/image/docker->crypto/tls --> +<g id="edge26" class="edge"> +<title>github.com/containers/image/docker->crypto/tls</title> +<path fill="none" stroke="#000000" d="M108.0767,-9015.9112C110.1478,-9504.7959 154.4374,-19621.5528 274,-19891.5998 397.4799,-20170.4945 469.9926,-20360.5998 775,-20360.5998 775,-20360.5998 775,-20360.5998 3354.5,-20360.5998 3705.1724,-20360.5998 3810.3455,-20188.8236 3983,-19883.5998 4100.7565,-19675.4264 3961.581,-19574.1999 4041,-19348.5998 4049.3006,-19325.0208 4065.1505,-19301.3178 4077.7111,-19284.7852"/> +<polygon fill="#000000" stroke="#000000" points="4079.1616,-19285.7698 4080.827,-19280.741 4076.3891,-19283.6337 4079.1616,-19285.7698"/> +</g> +<!-- github.com/containers/image/docker->encoding/json --> +<g id="edge27" class="edge"> +<title>github.com/containers/image/docker->encoding/json</title> +<path fill="none" stroke="#000000" d="M108.0966,-9015.6621C110.3777,-9436.2375 153.3824,-17064.8233 274,-17508.5998 393.3606,-17947.7516 319.9162,-18401.5998 775,-18401.5998 775,-18401.5998 775,-18401.5998 2576.5,-18401.5998 2581.5534,-18401.5998 3979.6433,-18031.3773 3983,-18027.5998 4009.3524,-17997.9441 4079.0234,-17388.802 4093.3874,-17260.9947"/> +<polygon fill="#000000" stroke="#000000" points="4095.1338,-17261.1243 4093.9526,-17255.9602 4091.6556,-17260.7338 4095.1338,-17261.1243"/> +</g> +<!-- github.com/containers/image/docker->errors --> +<g id="edge28" class="edge"> +<title>github.com/containers/image/docker->errors</title> +<path fill="none" stroke="#000000" d="M108.1072,-8979.2185C109.9132,-8681.6996 135.7309,-4869.8585 274,-4685.5998 414.8757,-4497.8675 540.2887,-4518.5998 775,-4518.5998 775,-4518.5998 775,-4518.5998 1848.5,-4518.5998 2611.1132,-4518.5998 2961.7325,-4380.2225 3522,-4897.5998 3585.6168,-4956.3465 3513.4112,-5028.2446 3580,-5083.5998 3718.7083,-5198.9077 3858.6961,-5000.8923 3983,-5131.5998 4041.8152,-5193.445 4088.8126,-6563.2577 4095.248,-6759.3198"/> +<polygon fill="#000000" stroke="#000000" points="4093.5054,-6759.5768 4095.4181,-6764.5169 4097.0035,-6759.4623 4093.5054,-6759.5768"/> +</g> +<!-- github.com/containers/image/docker->fmt --> +<g id="edge29" class="edge"> +<title>github.com/containers/image/docker->fmt</title> +<path fill="none" stroke="#000000" d="M114.1774,-8979.4412C131.1964,-8932.4778 183.5937,-8806.7662 274,-8750.5998 396.6406,-8674.4073 452.2222,-8720.7897 596,-8707.5998 1744.117,-8602.2738 2034.3194,-8604.9615 3187,-8580.5998 3363.8583,-8576.8619 3825.7673,-8507.5398 3983,-8588.5998 4036.8519,-8616.3627 4071.5562,-8682.921 4086.8974,-8718.404"/> +<polygon fill="#000000" stroke="#000000" points="4085.4558,-8719.4867 4089.0203,-8723.4054 4088.6775,-8718.1191 4085.4558,-8719.4867"/> +</g> +<!-- github.com/containers/image/docker->io --> +<g id="edge49" class="edge"> +<title>github.com/containers/image/docker->io</title> +<path fill="none" stroke="#000000" d="M108.2346,-9015.7509C113.0578,-9387.1364 191.5189,-15341.1846 274,-16101.5998 362.1176,-16913.9797 -42.1449,-17870.5998 775,-17870.5998 775,-17870.5998 775,-17870.5998 1848.5,-17870.5998 2129.1558,-17870.5998 2165.7332,-17706.1655 2368,-17511.5998 2590.6142,-17297.4614 2592.7057,-17197.3336 2785,-16955.5998 3299.2459,-16309.1397 3685.7345,-16305.3091 3983,-15534.5998 4047.8169,-15366.5513 3949.4672,-14069.7232 4041,-13914.5998 4046.3495,-13905.5339 4055.2478,-13898.6637 4064.3445,-13893.6038"/> +<polygon fill="#000000" stroke="#000000" points="4065.1946,-13895.1341 4068.8115,-13891.2635 4063.5703,-13892.0338 4065.1946,-13895.1341"/> +</g> +<!-- github.com/containers/image/docker->io/ioutil --> +<g id="edge50" class="edge"> +<title>github.com/containers/image/docker->io/ioutil</title> +<path fill="none" stroke="#000000" d="M108.0157,-9015.8874C108.4032,-9412.6285 117.276,-16140.2335 274,-16987.5998 368.769,-17499.9912 253.9183,-18047.5998 775,-18047.5998 775,-18047.5998 775,-18047.5998 2195.5,-18047.5998 2250.2529,-18047.5998 3531.182,-16669.3926 3580,-16644.5998 3741.4568,-16562.6023 3844.9948,-16701.8451 3983,-16584.5998 4047.3774,-16529.9066 4008.0824,-16481.3959 4041,-16403.5998 4053.1176,-16374.9616 4070.4612,-16343.6614 4082.417,-16323.1721"/> +<polygon fill="#000000" stroke="#000000" points="4084.0114,-16323.9127 4085.0326,-16318.7146 4080.9927,-16322.1413 4084.0114,-16323.9127"/> +</g> +<!-- github.com/containers/image/docker->strconv --> +<g id="edge57" class="edge"> +<title>github.com/containers/image/docker->strconv</title> +<path fill="none" stroke="#000000" d="M108.1139,-8979.2729C110.0062,-8686.3457 136.6448,-4974.0993 274,-4515.5998 353.2054,-4251.2075 452.0744,-4220.104 596,-3984.5998 1007.0122,-3312.0646 737.8157,-2474.5998 1526,-2474.5998 1526,-2474.5998 1526,-2474.5998 1848.5,-2474.5998 2909.975,-2474.5998 2305.4453,-3861.338 3187,-4452.5998 3335.1846,-4551.9878 3404.4668,-4502.5881 3580,-4534.5998 3669.1725,-4550.8621 3919.8183,-4532.6054 3983,-4597.5998 4054.0484,-4670.6867 4025.3295,-4951.8823 4041,-5052.5998 4057.0498,-5155.7551 4081.131,-5277.9985 4091.2521,-5328.2549"/> +<polygon fill="#000000" stroke="#000000" points="4089.5974,-5328.9023 4092.3018,-5333.4574 4093.0282,-5328.21 4089.5974,-5328.9023"/> +</g> +<!-- github.com/containers/image/docker->strings --> +<g id="edge58" class="edge"> +<title>github.com/containers/image/docker->strings</title> +<path fill="none" stroke="#000000" d="M108.2736,-8979.2299C112.4407,-8703.7582 164.3605,-5420.2252 274,-5253.5998 663.9815,-4660.9225 1139.027,-4990.5998 1848.5,-4990.5998 1848.5,-4990.5998 1848.5,-4990.5998 2195.5,-4990.5998 2452.8914,-4990.5998 2523.8118,-5062.597 2727,-5220.5998 2859.0727,-5323.3019 3058.8379,-5709.0573 3187,-5816.5998 3224.7672,-5848.2908 3534.4124,-6003.8262 3580,-6022.5998 3753.2799,-6093.9588 3865.2545,-6000.8122 3983,-6146.5998 4076.7603,-6262.6899 4019.1873,-6667.9783 4041,-6815.5998 4054.1488,-6904.5867 4078.8522,-7008.7721 4090.1238,-7054.3135"/> +<polygon fill="#000000" stroke="#000000" points="4088.4619,-7054.8827 4091.3657,-7059.3133 4091.8587,-7054.0389 4088.4619,-7054.8827"/> +</g> +<!-- github.com/containers/image/docker->sync --> +<g id="edge59" class="edge"> +<title>github.com/containers/image/docker->sync</title> +<path fill="none" stroke="#000000" d="M108.1876,-8979.303C112.3165,-8579.1193 184.1005,-1746.6952 274,-1567.5998 400.7129,-1315.1657 492.548,-1176.5998 775,-1176.5998 775,-1176.5998 775,-1176.5998 2957,-1176.5998 3229.6735,-1176.5998 3854.6897,-1609.002 3983,-1849.5998 4080.0213,-2031.5269 4020.9347,-2570.3975 4041,-2775.5998 4055.5841,-2924.7478 4082.5844,-3102.9788 4092.3528,-3165.5379"/> +<polygon fill="#000000" stroke="#000000" points="4090.633,-3165.8673 4093.1352,-3170.5365 4094.0909,-3165.3261 4090.633,-3165.8673"/> +</g> +<!-- github.com/containers/image/docker->time --> +<g id="edge60" class="edge"> +<title>github.com/containers/image/docker->time</title> +<path fill="none" stroke="#000000" d="M108.3298,-9015.8248C111.9426,-9207.6293 146.5066,-10858.201 274,-11330.5998 405.7052,-11818.6044 269.5351,-12351.5998 775,-12351.5998 775,-12351.5998 775,-12351.5998 1848.5,-12351.5998 2492.161,-12351.5998 2590.9467,-12041.5401 3187,-11798.5998 3230.5825,-11780.8364 3534.7788,-11653.6391 3580,-11640.5998 3754.9003,-11590.1684 3832.4785,-11669.9556 3983,-11567.5998 4040.8369,-11528.2703 4074.6786,-11448.5105 4088.5681,-11408.9437"/> +<polygon fill="#000000" stroke="#000000" points="4090.3438,-11409.162 4090.3186,-11403.8646 4087.0348,-11408.0215 4090.3438,-11409.162"/> +</g> +<!-- github.com/containers/image/v5/docker/policyconfiguration --> +<g id="node43" class="node"> +<title>github.com/containers/image/v5/docker/policyconfiguration</title> +<g id="a_node43"><a xlink:href="https://godoc.org/github.com/containers/image/v5/docker/policyconfiguration" xlink:title="github.com/containers/image/v5/docker/policyconfiguration" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3117,-7806.5998C3117,-7806.5998 2797,-7806.5998 2797,-7806.5998 2791,-7806.5998 2785,-7800.5998 2785,-7794.5998 2785,-7794.5998 2785,-7782.5998 2785,-7782.5998 2785,-7776.5998 2791,-7770.5998 2797,-7770.5998 2797,-7770.5998 3117,-7770.5998 3117,-7770.5998 3123,-7770.5998 3129,-7776.5998 3129,-7782.5998 3129,-7782.5998 3129,-7794.5998 3129,-7794.5998 3129,-7800.5998 3123,-7806.5998 3117,-7806.5998"/> +<text text-anchor="middle" x="2957" y="-7784.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/docker/policyconfiguration</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/docker/policyconfiguration --> +<g id="edge30" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/docker/policyconfiguration</title> +<path fill="none" stroke="#000000" d="M110.078,-8979.5352C118.2169,-8917.3259 153.6629,-8710.0201 274,-8604.5998 759.3146,-8179.4442 1059.3606,-8416.8293 1674,-8220.5998 2174.8116,-8060.711 2275.653,-7944.7624 2785,-7814.5998 2794.8067,-7812.0937 2804.9937,-7809.7982 2815.2933,-7807.7011"/> +<polygon fill="#000000" stroke="#000000" points="2815.7982,-7809.3848 2820.3574,-7806.6874 2815.1112,-7805.9529 2815.7982,-7809.3848"/> +</g> +<!-- github.com/containers/image/v5/docker/reference --> +<g id="node44" class="node"> +<title>github.com/containers/image/v5/docker/reference</title> +<g id="a_node44"><a xlink:href="https://godoc.org/github.com/containers/image/v5/docker/reference" xlink:title="github.com/containers/image/v5/docker/reference" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3486,-6679.5998C3486,-6679.5998 3223,-6679.5998 3223,-6679.5998 3217,-6679.5998 3211,-6673.5998 3211,-6667.5998 3211,-6667.5998 3211,-6655.5998 3211,-6655.5998 3211,-6649.5998 3217,-6643.5998 3223,-6643.5998 3223,-6643.5998 3486,-6643.5998 3486,-6643.5998 3492,-6643.5998 3498,-6649.5998 3498,-6655.5998 3498,-6655.5998 3498,-6667.5998 3498,-6667.5998 3498,-6673.5998 3492,-6679.5998 3486,-6679.5998"/> +<text text-anchor="middle" x="3354.5" y="-6657.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/docker/reference</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/docker/reference --> +<g id="edge31" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M108.3552,-8979.4036C114.2215,-8681.0041 191.8455,-4811.6189 274,-4732.5998 358.5652,-4651.262 421.5999,-4717.8303 538,-4732.5998 954.635,-4785.4648 1981.3579,-5040.6263 2368,-5204.5998 2540.2796,-5277.6629 2630.619,-5264.1967 2727,-5424.5998 2835.7051,-5605.5135 2641.654,-6217.6853 2785,-6372.5998 2889.8435,-6485.9046 3008.0242,-6324.7093 3129,-6420.5998 3185.0145,-6464.9993 3139.8133,-6516.9123 3187,-6570.5998 3215.959,-6603.5485 3259.834,-6626.7733 3295.4155,-6641.5106"/> +<polygon fill="#000000" stroke="#000000" points="3294.8652,-6643.1761 3300.1559,-6643.4444 3296.1873,-6639.9353 3294.8652,-6643.1761"/> +</g> +<!-- github.com/containers/image/v5/image --> +<g id="node45" class="node"> +<title>github.com/containers/image/v5/image</title> +<g id="a_node45"><a xlink:href="https://godoc.org/github.com/containers/image/v5/image" xlink:title="github.com/containers/image/v5/image" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1951.5,-10920.5998C1951.5,-10920.5998 1745.5,-10920.5998 1745.5,-10920.5998 1739.5,-10920.5998 1733.5,-10914.5998 1733.5,-10908.5998 1733.5,-10908.5998 1733.5,-10896.5998 1733.5,-10896.5998 1733.5,-10890.5998 1739.5,-10884.5998 1745.5,-10884.5998 1745.5,-10884.5998 1951.5,-10884.5998 1951.5,-10884.5998 1957.5,-10884.5998 1963.5,-10890.5998 1963.5,-10896.5998 1963.5,-10896.5998 1963.5,-10908.5998 1963.5,-10908.5998 1963.5,-10914.5998 1957.5,-10920.5998 1951.5,-10920.5998"/> +<text text-anchor="middle" x="1848.5" y="-10898.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/image</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/image --> +<g id="edge32" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/image</title> +<path fill="none" stroke="#000000" d="M108.3103,-9015.6884C111.6199,-9199.0476 142.9189,-10717.6288 274,-10862.5998 426.0548,-11030.767 548.2824,-10958.5998 775,-10958.5998 775,-10958.5998 775,-10958.5998 1166,-10958.5998 1363.4893,-10958.5998 1592.0496,-10935.0211 1728.2055,-10918.4633"/> +<polygon fill="#000000" stroke="#000000" points="1728.6604,-10920.1708 1733.4117,-10917.8281 1728.2365,-10916.6966 1728.6604,-10920.1708"/> +</g> +<!-- github.com/containers/image/v5/manifest --> +<g id="node46" class="node"> +<title>github.com/containers/image/v5/manifest</title> +<g id="a_node46"><a xlink:href="https://godoc.org/github.com/containers/image/v5/manifest" xlink:title="github.com/containers/image/v5/manifest" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2305.5,-11204.5998C2305.5,-11204.5998 2085.5,-11204.5998 2085.5,-11204.5998 2079.5,-11204.5998 2073.5,-11198.5998 2073.5,-11192.5998 2073.5,-11192.5998 2073.5,-11180.5998 2073.5,-11180.5998 2073.5,-11174.5998 2079.5,-11168.5998 2085.5,-11168.5998 2085.5,-11168.5998 2305.5,-11168.5998 2305.5,-11168.5998 2311.5,-11168.5998 2317.5,-11174.5998 2317.5,-11180.5998 2317.5,-11180.5998 2317.5,-11192.5998 2317.5,-11192.5998 2317.5,-11198.5998 2311.5,-11204.5998 2305.5,-11204.5998"/> +<text text-anchor="middle" x="2195.5" y="-11182.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/manifest</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/manifest --> +<g id="edge33" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/manifest</title> +<path fill="none" stroke="#000000" d="M108.7042,-9015.8208C116.0162,-9202.1923 179.7214,-10759.1907 274,-10939.5998 408.0461,-11196.1071 485.5793,-11355.5998 775,-11355.5998 775,-11355.5998 775,-11355.5998 1526,-11355.5998 1766.97,-11355.5998 2042.2948,-11251.4042 2149.4552,-11206.6422"/> +<polygon fill="#000000" stroke="#000000" points="2150.1876,-11208.2328 2154.1223,-11204.6858 2148.8345,-11205.0049 2150.1876,-11208.2328"/> +</g> +<!-- github.com/containers/image/v5/pkg/blobinfocache/none --> +<g id="node47" class="node"> +<title>github.com/containers/image/v5/pkg/blobinfocache/none</title> +<g id="a_node47"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/blobinfocache/none" xlink:title="github.com/containers/image/v5/pkg/blobinfocache/none" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2347,-10680.5998C2347,-10680.5998 2044,-10680.5998 2044,-10680.5998 2038,-10680.5998 2032,-10674.5998 2032,-10668.5998 2032,-10668.5998 2032,-10656.5998 2032,-10656.5998 2032,-10650.5998 2038,-10644.5998 2044,-10644.5998 2044,-10644.5998 2347,-10644.5998 2347,-10644.5998 2353,-10644.5998 2359,-10650.5998 2359,-10656.5998 2359,-10656.5998 2359,-10668.5998 2359,-10668.5998 2359,-10674.5998 2353,-10680.5998 2347,-10680.5998"/> +<text text-anchor="middle" x="2195.5" y="-10658.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/blobinfocache/none</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/pkg/blobinfocache/none --> +<g id="edge34" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/pkg/blobinfocache/none</title> +<path fill="none" stroke="#000000" d="M108.8666,-9016.0789C113.2303,-9089.8803 138.3683,-9367.5627 274,-9534.5998 434.5189,-9732.2867 520.3504,-9812.5998 775,-9812.5998 775,-9812.5998 775,-9812.5998 1526,-9812.5998 1959.5196,-9812.5998 2155.5055,-10503.3876 2189.9104,-10639.481"/> +<polygon fill="#000000" stroke="#000000" points="2188.2436,-10640.0292 2191.1576,-10644.4531 2191.6385,-10639.1776 2188.2436,-10640.0292"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config --> +<g id="node48" class="node"> +<title>github.com/containers/image/v5/pkg/docker/config</title> +<g id="a_node48"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/docker/config" xlink:title="github.com/containers/image/v5/pkg/docker/config" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1662,-15988.5998C1662,-15988.5998 1390,-15988.5998 1390,-15988.5998 1384,-15988.5998 1378,-15982.5998 1378,-15976.5998 1378,-15976.5998 1378,-15964.5998 1378,-15964.5998 1378,-15958.5998 1384,-15952.5998 1390,-15952.5998 1390,-15952.5998 1662,-15952.5998 1662,-15952.5998 1668,-15952.5998 1674,-15958.5998 1674,-15964.5998 1674,-15964.5998 1674,-15976.5998 1674,-15976.5998 1674,-15982.5998 1668,-15988.5998 1662,-15988.5998"/> +<text text-anchor="middle" x="1526" y="-15966.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/docker/config</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/pkg/docker/config --> +<g id="edge35" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/pkg/docker/config</title> +<path fill="none" stroke="#000000" d="M108.2604,-9015.6641C113.0066,-9342.3197 182.1231,-13992.7789 274,-14259.5998 551.6618,-15065.9614 1352.279,-15814.2505 1501.7963,-15949.0664"/> +<polygon fill="#000000" stroke="#000000" points="1500.7322,-15950.4631 1505.6192,-15952.5075 1503.0738,-15947.8617 1500.7322,-15950.4631"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2 --> +<g id="node49" class="node"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2</title> +<g id="a_node49"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/sysregistriesv2" xlink:title="github.com/containers/image/v5/pkg/sysregistriesv2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2715,-8750.5998C2715,-8750.5998 2438,-8750.5998 2438,-8750.5998 2432,-8750.5998 2426,-8744.5998 2426,-8738.5998 2426,-8738.5998 2426,-8726.5998 2426,-8726.5998 2426,-8720.5998 2432,-8714.5998 2438,-8714.5998 2438,-8714.5998 2715,-8714.5998 2715,-8714.5998 2721,-8714.5998 2727,-8720.5998 2727,-8726.5998 2727,-8726.5998 2727,-8738.5998 2727,-8738.5998 2727,-8744.5998 2721,-8750.5998 2715,-8750.5998"/> +<text text-anchor="middle" x="2576.5" y="-8728.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/sysregistriesv2</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/pkg/sysregistriesv2 --> +<g id="edge36" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/pkg/sysregistriesv2</title> +<path fill="none" stroke="#000000" d="M118.676,-8979.4546C141.6117,-8942.4559 199.5985,-8858.9825 274,-8825.5998 658.1473,-8653.2394 1967.1679,-8701.4421 2420.4742,-8724.0295"/> +<polygon fill="#000000" stroke="#000000" points="2420.6864,-8725.7922 2425.7676,-8724.294 2420.8612,-8722.2965 2420.6864,-8725.7922"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig --> +<g id="node50" class="node"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig</title> +<g id="a_node50"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/tlsclientconfig" xlink:title="github.com/containers/image/v5/pkg/tlsclientconfig" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2713.5,-16696.5998C2713.5,-16696.5998 2439.5,-16696.5998 2439.5,-16696.5998 2433.5,-16696.5998 2427.5,-16690.5998 2427.5,-16684.5998 2427.5,-16684.5998 2427.5,-16672.5998 2427.5,-16672.5998 2427.5,-16666.5998 2433.5,-16660.5998 2439.5,-16660.5998 2439.5,-16660.5998 2713.5,-16660.5998 2713.5,-16660.5998 2719.5,-16660.5998 2725.5,-16666.5998 2725.5,-16672.5998 2725.5,-16672.5998 2725.5,-16684.5998 2725.5,-16684.5998 2725.5,-16690.5998 2719.5,-16696.5998 2713.5,-16696.5998"/> +<text text-anchor="middle" x="2576.5" y="-16674.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/tlsclientconfig</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/pkg/tlsclientconfig --> +<g id="edge37" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/pkg/tlsclientconfig</title> +<path fill="none" stroke="#000000" d="M108.2282,-9015.9753C112.0749,-9321.9297 164.6577,-13361.0434 274,-14541.5998 340.6035,-15260.7108 52.8112,-17752.5998 775,-17752.5998 775,-17752.5998 775,-17752.5998 1848.5,-17752.5998 2167.4195,-17752.5998 2219.4798,-17539.8256 2368,-17257.5998 2443.6769,-17113.7947 2365.458,-17047.4029 2426,-16896.5998 2457.9064,-16817.1245 2523.0268,-16737.9235 2556.1593,-16700.6491"/> +<polygon fill="#000000" stroke="#000000" points="2557.6229,-16701.6377 2559.6505,-16696.7436 2555.0135,-16699.305 2557.6229,-16701.6377"/> +</g> +<!-- github.com/containers/image/v5/transports --> +<g id="node51" class="node"> +<title>github.com/containers/image/v5/transports</title> +<g id="a_node51"><a xlink:href="https://godoc.org/github.com/containers/image/v5/transports" xlink:title="github.com/containers/image/v5/transports" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2689.5,-2551.5998C2689.5,-2551.5998 2463.5,-2551.5998 2463.5,-2551.5998 2457.5,-2551.5998 2451.5,-2545.5998 2451.5,-2539.5998 2451.5,-2539.5998 2451.5,-2527.5998 2451.5,-2527.5998 2451.5,-2521.5998 2457.5,-2515.5998 2463.5,-2515.5998 2463.5,-2515.5998 2689.5,-2515.5998 2689.5,-2515.5998 2695.5,-2515.5998 2701.5,-2521.5998 2701.5,-2527.5998 2701.5,-2527.5998 2701.5,-2539.5998 2701.5,-2539.5998 2701.5,-2545.5998 2695.5,-2551.5998 2689.5,-2551.5998"/> +<text text-anchor="middle" x="2576.5" y="-2529.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/transports</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/transports --> +<g id="edge38" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/transports</title> +<path fill="none" stroke="#000000" d="M108.1152,-8979.3183C110.5292,-8602.4073 151.3587,-2522.3495 274,-2393.5998 427.9962,-2231.9334 551.7269,-2356.5998 775,-2356.5998 775,-2356.5998 775,-2356.5998 1848.5,-2356.5998 2083.298,-2356.5998 2142.1292,-2388.4711 2368,-2452.5998 2425.9129,-2469.0423 2490.145,-2495.2744 2531.8055,-2513.4099"/> +<polygon fill="#000000" stroke="#000000" points="2531.2375,-2515.0715 2536.52,-2515.4696 2532.6388,-2511.8642 2531.2375,-2515.0715"/> +</g> +<!-- github.com/containers/image/v5/types --> +<g id="node52" class="node"> +<title>github.com/containers/image/v5/types</title> +<g id="a_node52"><a xlink:href="https://godoc.org/github.com/containers/image/v5/types" xlink:title="github.com/containers/image/v5/types" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3057.5,-10920.5998C3057.5,-10920.5998 2856.5,-10920.5998 2856.5,-10920.5998 2850.5,-10920.5998 2844.5,-10914.5998 2844.5,-10908.5998 2844.5,-10908.5998 2844.5,-10896.5998 2844.5,-10896.5998 2844.5,-10890.5998 2850.5,-10884.5998 2856.5,-10884.5998 2856.5,-10884.5998 3057.5,-10884.5998 3057.5,-10884.5998 3063.5,-10884.5998 3069.5,-10890.5998 3069.5,-10896.5998 3069.5,-10896.5998 3069.5,-10908.5998 3069.5,-10908.5998 3069.5,-10914.5998 3063.5,-10920.5998 3057.5,-10920.5998"/> +<text text-anchor="middle" x="2957" y="-10898.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/types</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/containers/image/v5/types --> +<g id="edge39" class="edge"> +<title>github.com/containers/image/docker->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M110.3783,-9015.8406C135.8235,-9207.2489 364.9686,-10840.5998 775,-10840.5998 775,-10840.5998 775,-10840.5998 2195.5,-10840.5998 2423.9207,-10840.5998 2689.6151,-10868.6553 2839.0738,-10887.0212"/> +<polygon fill="#000000" stroke="#000000" points="2839.1647,-10888.7956 2844.3412,-10887.6704 2839.5929,-10885.3218 2839.1647,-10888.7956"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode --> +<g id="node53" class="node"> +<title>github.com/docker/distribution/registry/api/errcode</title> +<g id="a_node53"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3917,-4519.5998C3917,-4519.5998 3646,-4519.5998 3646,-4519.5998 3640,-4519.5998 3634,-4513.5998 3634,-4507.5998 3634,-4507.5998 3634,-4495.5998 3634,-4495.5998 3634,-4489.5998 3640,-4483.5998 3646,-4483.5998 3646,-4483.5998 3917,-4483.5998 3917,-4483.5998 3923,-4483.5998 3929,-4489.5998 3929,-4495.5998 3929,-4495.5998 3929,-4507.5998 3929,-4507.5998 3929,-4513.5998 3923,-4519.5998 3917,-4519.5998"/> +<text text-anchor="middle" x="3781.5" y="-4497.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/docker/distribution/registry/api/errcode --> +<g id="edge40" class="edge"> +<title>github.com/containers/image/docker->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M108.1213,-8979.4687C110.7423,-8592.9779 156.1523,-2155.5427 274,-2008.5998 417.5137,-1829.6543 545.6146,-1884.5998 775,-1884.5998 775,-1884.5998 775,-1884.5998 2195.5,-1884.5998 2680.6938,-1884.5998 2881.715,-2033.1515 3129,-2450.5998 3203.1437,-2575.7638 3089.2949,-3650.8176 3187,-3758.5998 3288.0185,-3870.0371 3418.5542,-3697.412 3522,-3806.5998 3622.4991,-3912.6775 3482.3821,-4352.8648 3580,-4461.5998 3593.3435,-4476.4629 3610.4922,-4486.7372 3629.1426,-4493.7192"/> +<polygon fill="#000000" stroke="#000000" points="3628.6179,-4495.39 3633.9152,-4495.4242 3629.7954,-4492.094 3628.6179,-4495.39"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2 --> +<g id="node54" class="node"> +<title>github.com/docker/distribution/registry/api/v2</title> +<g id="a_node54"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/v2" xlink:title="github.com/docker/distribution/registry/api/v2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2698.5,-2256.5998C2698.5,-2256.5998 2454.5,-2256.5998 2454.5,-2256.5998 2448.5,-2256.5998 2442.5,-2250.5998 2442.5,-2244.5998 2442.5,-2244.5998 2442.5,-2232.5998 2442.5,-2232.5998 2442.5,-2226.5998 2448.5,-2220.5998 2454.5,-2220.5998 2454.5,-2220.5998 2698.5,-2220.5998 2698.5,-2220.5998 2704.5,-2220.5998 2710.5,-2226.5998 2710.5,-2232.5998 2710.5,-2232.5998 2710.5,-2244.5998 2710.5,-2244.5998 2710.5,-2250.5998 2704.5,-2256.5998 2698.5,-2256.5998"/> +<text text-anchor="middle" x="2576.5" y="-2234.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/v2</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/docker/distribution/registry/api/v2 --> +<g id="edge41" class="edge"> +<title>github.com/containers/image/docker->github.com/docker/distribution/registry/api/v2</title> +<path fill="none" stroke="#000000" d="M108.0949,-8979.3845C110.1401,-8594.3635 145.8592,-2233.2677 274,-2098.5998 427.9094,-1936.8507 551.7269,-2061.5998 775,-2061.5998 775,-2061.5998 775,-2061.5998 1848.5,-2061.5998 2111.8205,-2061.5998 2413.9364,-2172.2883 2528.8194,-2218.6012"/> +<polygon fill="#000000" stroke="#000000" points="2528.1968,-2220.2371 2533.4882,-2220.4895 2529.5092,-2216.9924 2528.1968,-2220.2371"/> +</g> +<!-- github.com/docker/distribution/registry/client --> +<g id="node55" class="node"> +<title>github.com/docker/distribution/registry/client</title> +<g id="a_node55"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client" xlink:title="github.com/docker/distribution/registry/client" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M526,-5303.5998C526,-5303.5998 286,-5303.5998 286,-5303.5998 280,-5303.5998 274,-5297.5998 274,-5291.5998 274,-5291.5998 274,-5279.5998 274,-5279.5998 274,-5273.5998 280,-5267.5998 286,-5267.5998 286,-5267.5998 526,-5267.5998 526,-5267.5998 532,-5267.5998 538,-5273.5998 538,-5279.5998 538,-5279.5998 538,-5291.5998 538,-5291.5998 538,-5297.5998 532,-5303.5998 526,-5303.5998"/> +<text text-anchor="middle" x="406" y="-5281.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/docker/distribution/registry/client --> +<g id="edge42" class="edge"> +<title>github.com/containers/image/docker->github.com/docker/distribution/registry/client</title> +<path fill="none" stroke="#000000" d="M109.4628,-8979.3782C130.8519,-8712.9475 379.9683,-5609.8599 404.1202,-5309.0152"/> +<polygon fill="#000000" stroke="#000000" points="405.8679,-5309.1129 404.5237,-5303.9889 402.3791,-5308.8328 405.8679,-5309.1129"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig --> +<g id="node56" class="node"> +<title>github.com/docker/go-connections/tlsconfig</title> +<g id="a_node56"><a xlink:href="https://godoc.org/github.com/docker/go-connections/tlsconfig" xlink:title="github.com/docker/go-connections/tlsconfig" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3471,-18573.5998C3471,-18573.5998 3238,-18573.5998 3238,-18573.5998 3232,-18573.5998 3226,-18567.5998 3226,-18561.5998 3226,-18561.5998 3226,-18549.5998 3226,-18549.5998 3226,-18543.5998 3232,-18537.5998 3238,-18537.5998 3238,-18537.5998 3471,-18537.5998 3471,-18537.5998 3477,-18537.5998 3483,-18543.5998 3483,-18549.5998 3483,-18549.5998 3483,-18561.5998 3483,-18561.5998 3483,-18567.5998 3477,-18573.5998 3471,-18573.5998"/> +<text text-anchor="middle" x="3354.5" y="-18551.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go-connections/tlsconfig</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/docker/go-connections/tlsconfig --> +<g id="edge43" class="edge"> +<title>github.com/containers/image/docker->github.com/docker/go-connections/tlsconfig</title> +<path fill="none" stroke="#000000" d="M108.168,-9015.8644C112.285,-9461.5843 191.4447,-17920.3886 274,-18423.5998 393.0573,-19149.3071 39.5915,-20000.5998 775,-20000.5998 775,-20000.5998 775,-20000.5998 2576.5,-20000.5998 3251.1579,-20000.5998 3343.3713,-18764.3283 3353.4117,-18578.789"/> +<polygon fill="#000000" stroke="#000000" points="3355.1691,-18578.6938 3353.6853,-18573.6084 3351.674,-18578.5091 3355.1691,-18578.6938"/> +</g> +<!-- github.com/ghodss/yaml --> +<g id="node57" class="node"> +<title>github.com/ghodss/yaml</title> +<g id="a_node57"><a xlink:href="https://godoc.org/github.com/ghodss/yaml" xlink:title="github.com/ghodss/yaml" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3418.5,-3743.5998C3418.5,-3743.5998 3290.5,-3743.5998 3290.5,-3743.5998 3284.5,-3743.5998 3278.5,-3737.5998 3278.5,-3731.5998 3278.5,-3731.5998 3278.5,-3719.5998 3278.5,-3719.5998 3278.5,-3713.5998 3284.5,-3707.5998 3290.5,-3707.5998 3290.5,-3707.5998 3418.5,-3707.5998 3418.5,-3707.5998 3424.5,-3707.5998 3430.5,-3713.5998 3430.5,-3719.5998 3430.5,-3719.5998 3430.5,-3731.5998 3430.5,-3731.5998 3430.5,-3737.5998 3424.5,-3743.5998 3418.5,-3743.5998"/> +<text text-anchor="middle" x="3354.5" y="-3721.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ghodss/yaml</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/ghodss/yaml --> +<g id="edge44" class="edge"> +<title>github.com/containers/image/docker->github.com/ghodss/yaml</title> +<path fill="none" stroke="#000000" d="M108.1856,-8979.4355C112.2039,-8588.9265 181.0419,-2030.9884 274,-1862.5998 403.0971,-1628.7473 507.88,-1530.5998 775,-1530.5998 775,-1530.5998 775,-1530.5998 2576.5,-1530.5998 2988.7282,-1530.5998 2969.9163,-1895.3046 3129,-2275.5998 3244.2526,-2551.1152 3337.5397,-3538.1731 3352.4261,-3702.3703"/> +<polygon fill="#000000" stroke="#000000" points="3350.69,-3702.6041 3352.8832,-3707.4262 3354.1757,-3702.2889 3350.69,-3702.6041"/> +</g> +<!-- github.com/opencontainers/go-digest --> +<g id="node58" class="node"> +<title>github.com/opencontainers/go-digest</title> +<g id="a_node58"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go-digest" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3878.5,-11040.5998C3878.5,-11040.5998 3684.5,-11040.5998 3684.5,-11040.5998 3678.5,-11040.5998 3672.5,-11034.5998 3672.5,-11028.5998 3672.5,-11028.5998 3672.5,-11016.5998 3672.5,-11016.5998 3672.5,-11010.5998 3678.5,-11004.5998 3684.5,-11004.5998 3684.5,-11004.5998 3878.5,-11004.5998 3878.5,-11004.5998 3884.5,-11004.5998 3890.5,-11010.5998 3890.5,-11016.5998 3890.5,-11016.5998 3890.5,-11028.5998 3890.5,-11028.5998 3890.5,-11034.5998 3884.5,-11040.5998 3878.5,-11040.5998"/> +<text text-anchor="middle" x="3781.5" y="-11018.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go-digest</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/opencontainers/go-digest --> +<g id="edge45" class="edge"> +<title>github.com/containers/image/docker->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M118.7683,-9015.627C174.7273,-9106.7783 442.5354,-9512.5998 775,-9512.5998 775,-9512.5998 775,-9512.5998 1526,-9512.5998 1775.8086,-9512.5998 1774.0857,-9702.4909 1965,-9863.5998 2337.9031,-10178.2857 2357.0559,-10359.1903 2785,-10593.5998 2924.3193,-10669.913 3009.1382,-10586.3563 3129,-10690.5998 3177.3521,-10732.6515 3137.1138,-10782.3799 3187,-10822.5998 3305.2732,-10917.9557 3396.1119,-10805.55 3522,-10890.5998 3560.8142,-10916.8226 3541.3799,-10952.092 3580,-10978.5998 3605.5353,-10996.1266 3636.8183,-11006.8327 3667.0499,-11013.3263"/> +<polygon fill="#000000" stroke="#000000" points="3667.0337,-11015.1101 3672.2848,-11014.4115 3667.7442,-11011.6829 3667.0337,-11015.1101"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="node59" class="node"> +<title>github.com/opencontainers/image-spec/specs-go/v1</title> +<g id="a_node59"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image-spec/specs-go/v1" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3492,-11187.5998C3492,-11187.5998 3217,-11187.5998 3217,-11187.5998 3211,-11187.5998 3205,-11181.5998 3205,-11175.5998 3205,-11175.5998 3205,-11163.5998 3205,-11163.5998 3205,-11157.5998 3211,-11151.5998 3217,-11151.5998 3217,-11151.5998 3492,-11151.5998 3492,-11151.5998 3498,-11151.5998 3504,-11157.5998 3504,-11163.5998 3504,-11163.5998 3504,-11175.5998 3504,-11175.5998 3504,-11181.5998 3498,-11187.5998 3492,-11187.5998"/> +<text text-anchor="middle" x="3354.5" y="-11165.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go/v1</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge46" class="edge"> +<title>github.com/containers/image/docker->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M108.5234,-9015.8308C114.2885,-9212.6764 167.7791,-10941.1649 274,-11139.5998 407.5569,-11389.1022 492.0002,-11532.5998 775,-11532.5998 775,-11532.5998 775,-11532.5998 2195.5,-11532.5998 2657.5235,-11532.5998 3177.7401,-11266.654 3318.0365,-11190.0296"/> +<polygon fill="#000000" stroke="#000000" points="3318.9267,-11191.5374 3322.4719,-11187.601 3317.2458,-11188.4675 3318.9267,-11191.5374"/> +</g> +<!-- github.com/pkg/errors --> +<g id="node60" class="node"> +<title>github.com/pkg/errors</title> +<g id="a_node60"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3838.5,-8821.5998C3838.5,-8821.5998 3724.5,-8821.5998 3724.5,-8821.5998 3718.5,-8821.5998 3712.5,-8815.5998 3712.5,-8809.5998 3712.5,-8809.5998 3712.5,-8797.5998 3712.5,-8797.5998 3712.5,-8791.5998 3718.5,-8785.5998 3724.5,-8785.5998 3724.5,-8785.5998 3838.5,-8785.5998 3838.5,-8785.5998 3844.5,-8785.5998 3850.5,-8791.5998 3850.5,-8797.5998 3850.5,-8797.5998 3850.5,-8809.5998 3850.5,-8809.5998 3850.5,-8815.5998 3844.5,-8821.5998 3838.5,-8821.5998"/> +<text text-anchor="middle" x="3781.5" y="-8799.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/pkg/errors --> +<g id="edge47" class="edge"> +<title>github.com/containers/image/docker->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M154.9007,-8979.5063C262.673,-8939.6109 537.0251,-8847.5998 775,-8847.5998 775,-8847.5998 775,-8847.5998 1848.5,-8847.5998 2443.5808,-8847.5998 2592.0518,-8826.1597 3187,-8813.5998 3374.1812,-8809.6483 3594.5401,-8806.2765 3707.1106,-8804.6464"/> +<polygon fill="#000000" stroke="#000000" points="3707.4149,-8806.3923 3712.389,-8804.5701 3707.3642,-8802.8926 3707.4149,-8806.3923"/> +</g> +<!-- github.com/sirupsen/logrus --> +<g id="node61" class="node"> +<title>github.com/sirupsen/logrus</title> +<g id="a_node61"><a xlink:href="https://godoc.org/github.com/sirupsen/logrus" xlink:title="github.com/sirupsen/logrus" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3425,-12323.5998C3425,-12323.5998 3284,-12323.5998 3284,-12323.5998 3278,-12323.5998 3272,-12317.5998 3272,-12311.5998 3272,-12311.5998 3272,-12299.5998 3272,-12299.5998 3272,-12293.5998 3278,-12287.5998 3284,-12287.5998 3284,-12287.5998 3425,-12287.5998 3425,-12287.5998 3431,-12287.5998 3437,-12293.5998 3437,-12299.5998 3437,-12299.5998 3437,-12311.5998 3437,-12311.5998 3437,-12317.5998 3431,-12323.5998 3425,-12323.5998"/> +<text text-anchor="middle" x="3354.5" y="-12301.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/sirupsen/logrus</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->github.com/sirupsen/logrus --> +<g id="edge48" class="edge"> +<title>github.com/containers/image/docker->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M108.2514,-9015.8007C111.9207,-9275.8874 156.256,-12238.0736 274,-12373.5998 422.6919,-12544.7478 548.2824,-12469.5998 775,-12469.5998 775,-12469.5998 775,-12469.5998 2195.5,-12469.5998 2604.9138,-12469.5998 3087.7261,-12368.0317 3274.8553,-12324.7571"/> +<polygon fill="#000000" stroke="#000000" points="3275.2872,-12326.4535 3279.7631,-12323.6198 3274.4971,-12323.0438 3275.2872,-12326.4535"/> +</g> +<!-- mime --> +<g id="node62" class="node"> +<title>mime</title> +<g id="a_node62"><a xlink:href="https://godoc.org/mime" xlink:title="mime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3369.5,-5067.5998C3369.5,-5067.5998 3339.5,-5067.5998 3339.5,-5067.5998 3333.5,-5067.5998 3327.5,-5061.5998 3327.5,-5055.5998 3327.5,-5055.5998 3327.5,-5043.5998 3327.5,-5043.5998 3327.5,-5037.5998 3333.5,-5031.5998 3339.5,-5031.5998 3339.5,-5031.5998 3369.5,-5031.5998 3369.5,-5031.5998 3375.5,-5031.5998 3381.5,-5037.5998 3381.5,-5043.5998 3381.5,-5043.5998 3381.5,-5055.5998 3381.5,-5055.5998 3381.5,-5061.5998 3375.5,-5067.5998 3369.5,-5067.5998"/> +<text text-anchor="middle" x="3354.5" y="-5045.8998" font-family="Times,serif" font-size="14.00" fill="#000000">mime</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->mime --> +<g id="edge51" class="edge"> +<title>github.com/containers/image/docker->mime</title> +<path fill="none" stroke="#000000" d="M108.0952,-8979.3436C110.1164,-8599.8414 144.9524,-2429.4869 274,-2303.5998 607.2088,-1978.5516 1913.6521,-2257.3451 2368,-2358.5998 2535.6353,-2395.9586 2623.626,-2364.4463 2727,-2501.5998 2804.6983,-2604.6875 2760.5299,-2952.8508 2785,-3079.5998 2942.8909,-3897.4368 3287.8458,-4865.8096 3346.0737,-5026.4931"/> +<polygon fill="#000000" stroke="#000000" points="3344.5183,-5027.3372 3347.8686,-5031.4406 3347.8085,-5026.1436 3344.5183,-5027.3372"/> +</g> +<!-- net/http --> +<g id="node63" class="node"> +<title>net/http</title> +<g id="a_node63"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4113,-598.5998C4113,-598.5998 4079,-598.5998 4079,-598.5998 4073,-598.5998 4067,-592.5998 4067,-586.5998 4067,-586.5998 4067,-574.5998 4067,-574.5998 4067,-568.5998 4073,-562.5998 4079,-562.5998 4079,-562.5998 4113,-562.5998 4113,-562.5998 4119,-562.5998 4125,-568.5998 4125,-574.5998 4125,-574.5998 4125,-586.5998 4125,-586.5998 4125,-592.5998 4119,-598.5998 4113,-598.5998"/> +<text text-anchor="middle" x="4096" y="-576.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->net/http --> +<g id="edge52" class="edge"> +<title>github.com/containers/image/docker->net/http</title> +<path fill="none" stroke="#000000" d="M108.0768,-8979.5222C109.9464,-8546.7436 146.4675,-486.5344 274,-288.5998 407.6219,-81.2142 528.2945,-49.5998 775,-49.5998 775,-49.5998 775,-49.5998 3354.5,-49.5998 3634.5484,-49.5998 3770.0591,87.2883 3983,-94.5998 4055.2503,-156.314 4086.654,-469.1587 4094.16,-557.3991"/> +<polygon fill="#000000" stroke="#000000" points="4092.4177,-557.5654 4094.5802,-562.4013 4095.9054,-557.2723 4092.4177,-557.5654"/> +</g> +<!-- net/url --> +<g id="node64" class="node"> +<title>net/url</title> +<g id="a_node64"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-1542.5998C4111,-1542.5998 4081,-1542.5998 4081,-1542.5998 4075,-1542.5998 4069,-1536.5998 4069,-1530.5998 4069,-1530.5998 4069,-1518.5998 4069,-1518.5998 4069,-1512.5998 4075,-1506.5998 4081,-1506.5998 4081,-1506.5998 4111,-1506.5998 4111,-1506.5998 4117,-1506.5998 4123,-1512.5998 4123,-1518.5998 4123,-1518.5998 4123,-1530.5998 4123,-1530.5998 4123,-1536.5998 4117,-1542.5998 4111,-1542.5998"/> +<text text-anchor="middle" x="4096" y="-1520.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->net/url --> +<g id="edge53" class="edge"> +<title>github.com/containers/image/docker->net/url</title> +<path fill="none" stroke="#000000" d="M108.1147,-8979.3969C110.7666,-8563.2628 159.6001,-1149.8696 274,-965.5998 406.4266,-752.2934 523.9294,-704.5998 775,-704.5998 775,-704.5998 775,-704.5998 2957,-704.5998 3437.948,-704.5998 3658.6364,-693.4953 3983,-1048.5998 4045.5604,-1117.0893 4083.6281,-1415.2326 4093.4549,-1501.2745"/> +<polygon fill="#000000" stroke="#000000" points="4091.7333,-1501.6247 4094.035,-1506.3959 4095.211,-1501.2307 4091.7333,-1501.6247"/> +</g> +<!-- os --> +<g id="node65" class="node"> +<title>os</title> +<g id="a_node65"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-18838.5998C4111,-18838.5998 4081,-18838.5998 4081,-18838.5998 4075,-18838.5998 4069,-18832.5998 4069,-18826.5998 4069,-18826.5998 4069,-18814.5998 4069,-18814.5998 4069,-18808.5998 4075,-18802.5998 4081,-18802.5998 4081,-18802.5998 4111,-18802.5998 4111,-18802.5998 4117,-18802.5998 4123,-18808.5998 4123,-18814.5998 4123,-18814.5998 4123,-18826.5998 4123,-18826.5998 4123,-18832.5998 4117,-18838.5998 4111,-18838.5998"/> +<text text-anchor="middle" x="4096" y="-18816.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->os --> +<g id="edge54" class="edge"> +<title>github.com/containers/image/docker->os</title> +<path fill="none" stroke="#000000" d="M108.1028,-9015.6188C110.761,-9477.4607 164.9863,-18667.3085 274,-19209.5998 360.8766,-19641.7698 334.1844,-20065.5998 775,-20065.5998 775,-20065.5998 775,-20065.5998 1848.5,-20065.5998 2323.6722,-20065.5998 3646.0034,-20273.5952 3983,-19938.5998 4045.8919,-19876.0814 4030.2999,-19230.6308 4041,-19142.5998 4054.6946,-19029.9322 4080.5898,-18896.6367 4091.2409,-18843.819"/> +<polygon fill="#000000" stroke="#000000" points="4093.0071,-18843.9138 4092.2832,-18838.6661 4089.5766,-18843.2198 4093.0071,-18843.9138"/> +</g> +<!-- path --> +<g id="node66" class="node"> +<title>path</title> +<g id="a_node66"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-2760.5998C4111,-2760.5998 4081,-2760.5998 4081,-2760.5998 4075,-2760.5998 4069,-2754.5998 4069,-2748.5998 4069,-2748.5998 4069,-2736.5998 4069,-2736.5998 4069,-2730.5998 4075,-2724.5998 4081,-2724.5998 4081,-2724.5998 4111,-2724.5998 4111,-2724.5998 4117,-2724.5998 4123,-2730.5998 4123,-2736.5998 4123,-2736.5998 4123,-2748.5998 4123,-2748.5998 4123,-2754.5998 4117,-2760.5998 4111,-2760.5998"/> +<text text-anchor="middle" x="4096" y="-2738.8998" font-family="Times,serif" font-size="14.00" fill="#000000">path</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->path --> +<g id="edge55" class="edge"> +<title>github.com/containers/image/docker->path</title> +<path fill="none" stroke="#000000" d="M108.1342,-8979.3688C111.1784,-8569.9911 166.0057,-1401.5608 274,-1221.5998 405.3662,-1002.6921 519.7008,-940.5998 775,-940.5998 775,-940.5998 775,-940.5998 2957,-940.5998 3468.8266,-940.5998 3701.1853,-1036.3447 3983,-1463.5998 4036.1603,-1544.1953 4031.3848,-2234.5311 4041,-2330.5998 4055.8888,-2479.3594 4082.7006,-2657.1757 4092.3861,-2719.5908"/> +<polygon fill="#000000" stroke="#000000" points="4090.664,-2719.9063 4093.1617,-2724.5779 4094.1224,-2719.3683 4090.664,-2719.9063"/> +</g> +<!-- path/filepath --> +<g id="node67" class="node"> +<title>path/filepath</title> +<g id="a_node67"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4126.5,-19398.5998C4126.5,-19398.5998 4065.5,-19398.5998 4065.5,-19398.5998 4059.5,-19398.5998 4053.5,-19392.5998 4053.5,-19386.5998 4053.5,-19386.5998 4053.5,-19374.5998 4053.5,-19374.5998 4053.5,-19368.5998 4059.5,-19362.5998 4065.5,-19362.5998 4065.5,-19362.5998 4126.5,-19362.5998 4126.5,-19362.5998 4132.5,-19362.5998 4138.5,-19368.5998 4138.5,-19374.5998 4138.5,-19374.5998 4138.5,-19386.5998 4138.5,-19386.5998 4138.5,-19392.5998 4132.5,-19398.5998 4126.5,-19398.5998"/> +<text text-anchor="middle" x="4096" y="-19376.8998" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text> +</a> +</g> +</g> +<!-- github.com/containers/image/docker->path/filepath --> +<g id="edge56" class="edge"> +<title>github.com/containers/image/docker->path/filepath</title> +<path fill="none" stroke="#000000" d="M108.0503,-9015.9265C109.4324,-9510.3739 139.9208,-19846.548 274,-20116.5998 396.1636,-20362.6521 500.2898,-20478.5998 775,-20478.5998 775,-20478.5998 775,-20478.5998 3354.5,-20478.5998 3638.003,-20478.5998 3788.4257,-20575.791 3983,-20369.5998 4051.6518,-20296.8492 4088.5525,-19546.77 4094.9813,-19403.9997"/> +<polygon fill="#000000" stroke="#000000" points="4096.7387,-19403.8695 4095.2141,-19398.7963 4093.2422,-19403.713 4096.7387,-19403.8695"/> +</g> +<!-- github.com/containers/image/v5/docker/policyconfiguration->strings --> +<g id="edge63" class="edge"> +<title>github.com/containers/image/v5/docker/policyconfiguration->strings</title> +<path fill="none" stroke="#000000" d="M2968.6847,-7770.4044C3000.1751,-7720.4445 3087.2395,-7576.0453 3129,-7443.5998 3155.2159,-7360.4546 3120.9544,-7112.5065 3187,-7055.5998 3321.0067,-6940.1361 3806.928,-7038.6195 3983,-7055.5998 4010.558,-7058.2575 4041.3784,-7064.4805 4063.8617,-7069.6323"/> +<polygon fill="#000000" stroke="#000000" points="4063.5487,-7071.3561 4068.8148,-7070.7808 4064.3394,-7067.9465 4063.5487,-7071.3561"/> +</g> +<!-- github.com/containers/image/v5/docker/policyconfiguration->github.com/containers/image/v5/docker/reference --> +<g id="edge61" class="edge"> +<title>github.com/containers/image/v5/docker/policyconfiguration->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M2960.5065,-7770.5879C2982.6087,-7657.8489 3104.3608,-7048.6054 3187,-6878.5998 3225.6437,-6799.1019 3296.3819,-6720.7348 3332.3033,-6683.7033"/> +<polygon fill="#000000" stroke="#000000" points="3333.8503,-6684.6236 3336.088,-6679.822 3331.3444,-6682.1801 3333.8503,-6684.6236"/> +</g> +<!-- github.com/containers/image/v5/docker/policyconfiguration->github.com/pkg/errors --> +<g id="edge62" class="edge"> +<title>github.com/containers/image/v5/docker/policyconfiguration->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2971.6607,-7806.6478C3071.6871,-7929.7852 3652.9872,-8645.3943 3763.3966,-8781.3137"/> +<polygon fill="#000000" stroke="#000000" points="3762.3147,-8782.7573 3766.8256,-8785.5349 3765.0313,-8780.5505 3762.3147,-8782.7573"/> +</g> +<!-- github.com/containers/image/v5/docker/reference->errors --> +<g id="edge64" class="edge"> +<title>github.com/containers/image/v5/docker/reference->errors</title> +<path fill="none" stroke="#000000" d="M3414.8302,-6643.4473C3451.3364,-6629.3641 3495.7599,-6606.1443 3522,-6570.5998 3599.5486,-6465.5536 3478.9696,-6365.3122 3580,-6282.5998 3718.5899,-6169.1379 3843.0845,-6170.7766 3983,-6282.5998 4059.5117,-6343.7495 4087.9909,-6669.3928 4094.4716,-6759.4852"/> +<polygon fill="#000000" stroke="#000000" points="4092.7338,-6759.7211 4094.8329,-6764.5849 4096.2251,-6759.4737 4092.7338,-6759.7211"/> +</g> +<!-- github.com/containers/image/v5/docker/reference->fmt --> +<g id="edge65" class="edge"> +<title>github.com/containers/image/v5/docker/reference->fmt</title> +<path fill="none" stroke="#000000" d="M3357.9512,-6679.7581C3384.7706,-6820.6805 3559.3827,-7734.7656 3580,-7754.5998 3710.85,-7880.4799 3857.3701,-7690.5096 3983,-7821.5998 4047.1771,-7888.5662 4087.4107,-8581.1102 4094.7748,-8718.1231"/> +<polygon fill="#000000" stroke="#000000" points="4093.0476,-8718.5979 4095.0621,-8723.4973 4096.5426,-8718.411 4093.0476,-8718.5979"/> +</g> +<!-- github.com/containers/image/v5/docker/reference->strings --> +<g id="edge69" class="edge"> +<title>github.com/containers/image/v5/docker/reference->strings</title> +<path fill="none" stroke="#000000" d="M3498.2613,-6645.2014C3637.7676,-6637.436 3847.6966,-6648.4041 3983,-6760.5998 4030.2308,-6799.7643 4076.0228,-6988.0367 4090.946,-7054.3925"/> +<polygon fill="#000000" stroke="#000000" points="4089.2501,-7054.8279 4092.049,-7059.3256 4092.6657,-7054.0642 4089.2501,-7054.8279"/> +</g> +<!-- github.com/containers/image/v5/docker/reference->github.com/opencontainers/go-digest --> +<g id="edge66" class="edge"> +<title>github.com/containers/image/v5/docker/reference->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M3359.2972,-6679.8553C3382.7744,-6770.1929 3485.8837,-7178.7365 3522,-7520.5998 3540.879,-7699.3009 3521.5477,-10584.6768 3580,-10754.5998 3616.467,-10860.6108 3712.1032,-10958.7915 3757.2458,-11000.9275"/> +<polygon fill="#000000" stroke="#000000" points="3756.0754,-11002.2289 3760.9309,-11004.3472 3758.4562,-10999.6633 3756.0754,-11002.2289"/> +</g> +<!-- github.com/containers/image/v5/docker/reference->path --> +<g id="edge67" class="edge"> +<title>github.com/containers/image/v5/docker/reference->path</title> +<path fill="none" stroke="#000000" d="M3420.9174,-6643.5807C3457.4949,-6629.9198 3499.8538,-6607.0981 3522,-6570.5998 3616.7152,-6414.5039 3510.7123,-3453.5262 3580,-3284.5998 3684.3451,-3030.2021 3968.831,-2825.9968 4064.2888,-2762.8662"/> +<polygon fill="#000000" stroke="#000000" points="4065.4772,-2764.179 4068.69,-2759.967 4063.5518,-2761.2561 4065.4772,-2764.179"/> +</g> +<!-- regexp --> +<g id="node68" class="node"> +<title>regexp</title> +<g id="a_node68"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-2315.5998C4111,-2315.5998 4081,-2315.5998 4081,-2315.5998 4075,-2315.5998 4069,-2309.5998 4069,-2303.5998 4069,-2303.5998 4069,-2291.5998 4069,-2291.5998 4069,-2285.5998 4075,-2279.5998 4081,-2279.5998 4081,-2279.5998 4111,-2279.5998 4111,-2279.5998 4117,-2279.5998 4123,-2285.5998 4123,-2291.5998 4123,-2291.5998 4123,-2303.5998 4123,-2303.5998 4123,-2309.5998 4117,-2315.5998 4111,-2315.5998"/> +<text text-anchor="middle" x="4096" y="-2293.8998" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/docker/reference->regexp --> +<g id="edge68" class="edge"> +<title>github.com/containers/image/v5/docker/reference->regexp</title> +<path fill="none" stroke="#000000" d="M3421.3991,-6643.4554C3457.8981,-6629.7881 3500.015,-6607.0023 3522,-6570.5998 3583.6837,-6468.4649 3495.0628,-2359.3977 3580,-2275.5998 3713.5662,-2143.8251 3973.5433,-2242.1765 4064.0308,-2282.4868"/> +<polygon fill="#000000" stroke="#000000" points="4063.4453,-2284.1422 4068.7235,-2284.5932 4064.8786,-2280.9491 4063.4453,-2284.1422"/> +</g> +<!-- github.com/containers/image/v5/image->bytes --> +<g id="edge70" class="edge"> +<title>github.com/containers/image/v5/image->bytes</title> +<path fill="none" stroke="#000000" d="M1853.2909,-10920.9917C1925.1103,-11196.6936 2780.3594,-14479.5545 2785,-14483.5998 2885.6542,-14571.3421 3884.908,-14669.1975 3983,-14578.5998 4042.6916,-14523.4687 4023.1907,-13928.8801 4041,-13849.5998 4051.0409,-13804.9013 4072.0324,-13755.563 4084.9814,-13727.5002"/> +<polygon fill="#000000" stroke="#000000" points="4086.6231,-13728.1201 4087.1444,-13722.8484 4083.4494,-13726.6444 4086.6231,-13728.1201"/> +</g> +<!-- github.com/containers/image/v5/image->context --> +<g id="edge71" class="edge"> +<title>github.com/containers/image/v5/image->context</title> +<path fill="none" stroke="#000000" d="M1848.585,-10884.4439C1849.8683,-10632.3596 1867.5275,-7805.057 2023,-5529.5998 2030.3525,-5421.9904 2370.4442,-1760.0519 2426,-1667.5998 2505.6188,-1535.1037 3432.1576,-935.7332 3580,-890.5998 3751.3064,-838.3034 3806.967,-857.5369 3983,-890.5998 4011.0096,-895.8607 4040.994,-907.8611 4063.0325,-918.0024"/> +<polygon fill="#000000" stroke="#000000" points="4062.3397,-919.6102 4067.6111,-920.1351 4063.8176,-916.4375 4062.3397,-919.6102"/> +</g> +<!-- github.com/containers/image/v5/image->crypto/sha256 --> +<g id="edge72" class="edge"> +<title>github.com/containers/image/v5/image->crypto/sha256</title> +<path fill="none" stroke="#000000" d="M1848.8658,-10921.0037C1854.8325,-11219.0464 1932.9638,-15043.4878 2023,-15529.5998 2124.0814,-16075.3456 1974.6312,-16388.6105 2426,-16711.5998 2960.5583,-17094.1177 3853.5105,-16704.248 4054.8406,-16607.9408"/> +<polygon fill="#000000" stroke="#000000" points="4055.7163,-16609.4617 4059.4673,-16605.7209 4054.2022,-16606.3061 4055.7163,-16609.4617"/> +</g> +<!-- github.com/containers/image/v5/image->encoding/hex --> +<g id="edge73" class="edge"> +<title>github.com/containers/image/v5/image->encoding/hex</title> +<path fill="none" stroke="#000000" d="M1855.3103,-10884.5502C1874.3511,-10836.4114 1932.7297,-10703.6682 2023,-10630.5998 2431.3295,-10300.0815 2803.6678,-10626.0732 3129,-10213.5998 3256.6122,-10051.8063 3079.5923,-9929.4566 3187,-9753.5998 3281.0058,-9599.6858 3417.7717,-9671.7837 3522,-9524.5998 3579.6935,-9443.1291 3504.8796,-9373.349 3580,-9307.5998 3621.2748,-9271.4741 3686.7572,-9269.6436 3731.8073,-9273.7733"/> +<polygon fill="#000000" stroke="#000000" points="3731.7752,-9275.5284 3736.9223,-9274.2757 3732.1174,-9272.0452 3731.7752,-9275.5284"/> +</g> +<!-- github.com/containers/image/v5/image->encoding/json --> +<g id="edge74" class="edge"> +<title>github.com/containers/image/v5/image->encoding/json</title> +<path fill="none" stroke="#000000" d="M1848.7302,-10920.8754C1852.7685,-11237.6297 1909.974,-15572.3999 2023,-16119.5998 2167.7245,-16820.2638 2250.5784,-17061.9227 2785,-17537.5998 2949.7549,-17684.2446 2977.4319,-17759.8241 3187,-17828.5998 3355.0708,-17883.757 3846.7323,-17944.3883 3983,-17831.5998 4073.1709,-17756.9655 4091.9927,-17361.5946 4095.3463,-17260.9327"/> +<polygon fill="#000000" stroke="#000000" points="4097.0992,-17260.8699 4095.5118,-17255.8159 4093.601,-17260.7567 4097.0992,-17260.8699"/> +</g> +<!-- github.com/containers/image/v5/image->fmt --> +<g id="edge75" class="edge"> +<title>github.com/containers/image/v5/image->fmt</title> +<path fill="none" stroke="#000000" d="M1849.7096,-10884.522C1856.9136,-10782.4914 1898.1711,-10269.0913 2023,-9871.5998 2148.7466,-9471.1863 2134.1248,-9319.1824 2426,-9017.5998 2897.2503,-8530.6764 3874.4881,-8697.0103 4063.8372,-8734.8263"/> +<polygon fill="#000000" stroke="#000000" points="4063.7013,-8736.5839 4068.9482,-8735.8542 4064.3914,-8733.1526 4063.7013,-8736.5839"/> +</g> +<!-- github.com/containers/image/v5/image->io/ioutil --> +<g id="edge84" class="edge"> +<title>github.com/containers/image/v5/image->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1848.6494,-10920.9724C1851.0537,-11207.1675 1883.5415,-14746.8655 2023,-15181.5998 2242.3903,-15865.5056 2513.6547,-16052.6692 3187,-16302.5998 3519.2952,-16425.9403 3636.4863,-16426.1737 3983,-16351.5998 4010.6987,-16345.6387 4039.7898,-16332.3233 4061.5176,-16320.793"/> +<polygon fill="#000000" stroke="#000000" points="4062.4634,-16322.2715 4066.0392,-16318.3629 4060.8065,-16319.1885 4062.4634,-16322.2715"/> +</g> +<!-- github.com/containers/image/v5/image->strings --> +<g id="edge85" class="edge"> +<title>github.com/containers/image/v5/image->strings</title> +<path fill="none" stroke="#000000" d="M1848.9611,-10884.5107C1853.4159,-10717.0107 1891.539,-9434.722 2023,-9077.5998 2227.1904,-8522.9037 2334.8771,-8370.7067 2785,-7987.5998 2914.275,-7877.5718 3033.0747,-7961.6588 3129,-7821.5998 3220.3295,-7688.2512 3069.3959,-7207.4699 3187,-7096.5998 3251.7708,-7035.5378 3908.1712,-7067.3336 4063.2787,-7075.7537"/> +<polygon fill="#000000" stroke="#000000" points="4063.4951,-7077.518 4068.583,-7076.0432 4063.6859,-7074.0232 4063.4951,-7077.518"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/containers/image/v5/docker/reference --> +<g id="edge76" class="edge"> +<title>github.com/containers/image/v5/image->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M1848.8119,-10884.4322C1853.8486,-10594.0444 1919.5673,-6913.7894 2023,-6449.5998 2123.1953,-5999.9391 2042.1066,-5749.2755 2426,-5494.5998 2537.4775,-5420.6454 2629.5022,-5402.9992 2727,-5494.5998 2877.7505,-5636.2321 2665.0425,-6255.0898 2785,-6423.5998 2881.8655,-6559.6715 3071.9006,-6618.2753 3205.7387,-6643.302"/> +<polygon fill="#000000" stroke="#000000" points="3205.6386,-6645.0631 3210.8734,-6644.2509 3206.2747,-6641.6213 3205.6386,-6645.0631"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/containers/image/v5/manifest --> +<g id="edge77" class="edge"> +<title>github.com/containers/image/v5/image->github.com/containers/image/v5/manifest</title> +<path fill="none" stroke="#000000" d="M1870.7343,-10920.7973C1932.0631,-10970.9915 2103.4247,-11111.2413 2169.2687,-11165.131"/> +<polygon fill="#000000" stroke="#000000" points="2168.4732,-11166.7413 2173.4509,-11168.5539 2170.69,-11164.0328 2168.4732,-11166.7413"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/containers/image/v5/pkg/blobinfocache/none --> +<g id="edge78" class="edge"> +<title>github.com/containers/image/v5/image->github.com/containers/image/v5/pkg/blobinfocache/none</title> +<path fill="none" stroke="#000000" d="M1868.2805,-10884.4444C1899.7289,-10856.0385 1963.655,-10800.1459 2023,-10759.5998 2066.0326,-10730.1987 2118.6332,-10701.5834 2154.3542,-10683.1454"/> +<polygon fill="#000000" stroke="#000000" points="2155.4153,-10684.5676 2159.0611,-10680.7243 2153.8143,-10681.4552 2155.4153,-10684.5676"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/containers/image/v5/types --> +<g id="edge79" class="edge"> +<title>github.com/containers/image/v5/image->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M1963.732,-10902.5998C2175.7003,-10902.5998 2625.2907,-10902.5998 2839.178,-10902.5998"/> +<polygon fill="#000000" stroke="#000000" points="2839.1864,-10904.3499 2844.1864,-10902.5998 2839.1864,-10900.8499 2839.1864,-10904.3499"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/opencontainers/go-digest --> +<g id="edge80" class="edge"> +<title>github.com/containers/image/v5/image->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M1963.9488,-10897.8497C1983.6499,-10897.0732 2003.9114,-10896.2958 2023,-10895.5998 2514.3546,-10877.6845 2692.335,-10644.6042 3129,-10870.5998 3168.6447,-10891.1179 3148.6251,-10929.795 3187,-10952.5998 3227.2959,-10976.5462 3506.0663,-11001.5072 3667.1677,-11014.1593"/> +<polygon fill="#000000" stroke="#000000" points="3667.2523,-11015.9212 3672.3737,-11014.567 3667.5257,-11012.4318 3667.2523,-11015.9212"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge81" class="edge"> +<title>github.com/containers/image/v5/image->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M1950.2397,-10920.6373C2223.0461,-10969.0034 2968.9862,-11101.2518 3247.6278,-11150.6524"/> +<polygon fill="#000000" stroke="#000000" points="3247.6816,-11152.4391 3252.9104,-11151.5889 3248.2927,-11148.9929 3247.6816,-11152.4391"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/pkg/errors --> +<g id="edge82" class="edge"> +<title>github.com/containers/image/v5/image->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M1854.8032,-10884.4508C1875.9502,-10824.1611 1947.3096,-10625.4906 2023,-10468.5998 2298.2011,-9898.1635 2468.1826,-9807.6541 2727,-9229.5998 2759.9671,-9155.9696 2723.5823,-9109.9077 2785,-9057.5998 2911.4102,-8949.9395 3386.4522,-9030.5011 3522,-8934.5998 3565.5216,-8903.8079 3535.9156,-8859.5804 3580,-8829.5998 3616.7065,-8804.6368 3666.0198,-8797.934 3706.674,-8797.5708"/> +<polygon fill="#000000" stroke="#000000" points="3707.0994,-8799.32 3712.0956,-8797.5591 3707.0918,-8795.82 3707.0994,-8799.32"/> +</g> +<!-- github.com/containers/image/v5/image->github.com/sirupsen/logrus --> +<g id="edge83" class="edge"> +<title>github.com/containers/image/v5/image->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M1855.8957,-10920.6842C1877.075,-10971.2145 1941.6911,-11116.9076 2023,-11219.5998 2439.4659,-11745.5915 3151.8673,-12185.5863 3318.7761,-12284.708"/> +<polygon fill="#000000" stroke="#000000" points="3318.3608,-12286.4962 3323.5542,-12287.5409 3320.1458,-12283.4856 3318.3608,-12286.4962"/> +</g> +<!-- github.com/containers/image/v5/manifest->encoding/json --> +<g id="edge88" class="edge"> +<title>github.com/containers/image/v5/manifest->encoding/json</title> +<path fill="none" stroke="#000000" d="M2196.367,-11204.7558C2206.5234,-11417.7322 2303.6499,-13462.5544 2368,-15116.5998 2371.6953,-15211.5826 2376.6984,-16745.3303 2426,-16826.5998 2505.6876,-16957.9582 2608.6825,-16898.5891 2727,-16996.5998 2753.6557,-17018.6806 3108.7557,-17444.5239 3129,-17472.5998 3157.9331,-17512.7257 3148.8917,-17536.0567 3187,-17567.5998 3330.7781,-17686.6083 3396.3764,-17682.1703 3580,-17715.5998 3668.1074,-17731.6401 3912.7671,-17771.1654 3983,-17715.5998 4056.2818,-17657.6221 4086.909,-17348.5848 4094.2061,-17260.8439"/> +<polygon fill="#000000" stroke="#000000" points="4095.9692,-17260.7545 4094.6339,-17255.6281 4092.4809,-17260.4683 4095.9692,-17260.7545"/> +</g> +<!-- github.com/containers/image/v5/manifest->fmt --> +<g id="edge89" class="edge"> +<title>github.com/containers/image/v5/manifest->fmt</title> +<path fill="none" stroke="#000000" d="M2204.1127,-11168.4193C2233.6514,-11105.0339 2331.0609,-10887.2297 2368,-10695.5998 2396.3759,-10548.3934 2358.485,-9481.4528 2426,-9347.5998 2748.517,-8708.1886 3859.8042,-8730.1435 4063.7212,-8739.7579"/> +<polygon fill="#000000" stroke="#000000" points="4063.8516,-8741.5161 4068.9304,-8740.0102 4064.021,-8738.0202 4063.8516,-8741.5161"/> +</g> +<!-- github.com/containers/image/v5/manifest->strings --> +<g id="edge104" class="edge"> +<title>github.com/containers/image/v5/manifest->strings</title> +<path fill="none" stroke="#000000" d="M2204.2158,-11168.4388C2234.091,-11105.117 2332.4679,-10887.4955 2368,-10695.5998 2388.7136,-10583.7335 2362.5595,-8743.0373 2426,-8648.5998 2507.5565,-8527.1949 2634.8464,-8629.1706 2727,-8515.5998 2805.4309,-8418.941 2717.3554,-8346.0916 2785,-8241.5998 2887.8508,-8082.7245 3036.5111,-8155.7224 3129,-7990.5998 3207.1181,-7851.1337 3086.4961,-7397.9067 3187,-7273.5998 3414.6808,-6991.9962 3625.7976,-7159.1448 3983,-7099.5998 4010.309,-7095.0475 4041.1499,-7088.9802 4063.7044,-7084.3704"/> +<polygon fill="#000000" stroke="#000000" points="4064.1282,-7086.07 4068.6745,-7083.3508 4063.4248,-7082.6414 4064.1282,-7086.07"/> +</g> +<!-- github.com/containers/image/v5/manifest->time --> +<g id="edge105" class="edge"> +<title>github.com/containers/image/v5/manifest->time</title> +<path fill="none" stroke="#000000" d="M2317.6508,-11190.176C2335.6683,-11194.7761 2353.1393,-11201.8808 2368,-11212.5998 2414.1051,-11245.8553 2386.4788,-11285.7381 2426,-11326.5998 2532.2107,-11436.413 2623.4613,-11380.2638 2727,-11492.5998 2769.6032,-11538.8229 2732.2172,-11587.4592 2785,-11621.5998 2913.3753,-11704.6348 2977.0542,-11638.5555 3129,-11621.5998 3322.3636,-11600.0223 3796.7246,-11484.783 3983,-11428.5998 4010.955,-11420.1682 4041.8942,-11408.2035 4064.3205,-11399.0348"/> +<polygon fill="#000000" stroke="#000000" points="4065.0132,-11400.6422 4068.9727,-11397.123 4063.6828,-11397.4049 4065.0132,-11400.6422"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/image/v5/docker/reference --> +<g id="edge90" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M2204.3073,-11168.4554C2234.4817,-11105.188 2333.7179,-10887.7228 2368,-10695.5998 2386.4908,-10591.9743 2363.9422,-6991.6233 2426,-6906.5998 2517.5308,-6781.1965 2967.3479,-6708.5828 3205.6657,-6678.3235"/> +<polygon fill="#000000" stroke="#000000" points="3206.1652,-6680.0244 3210.9061,-6677.6606 3205.726,-6676.552 3206.1652,-6680.0244"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/image/v5/types --> +<g id="edge93" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M2224.0787,-11168.5409C2266.7846,-11142.0957 2350.3432,-11092.4216 2426,-11059.5998 2579.9523,-10992.8115 2623.3441,-10987.6482 2785,-10942.5998 2811.6902,-10935.1621 2840.9717,-10927.9907 2867.6886,-10921.8245"/> +<polygon fill="#000000" stroke="#000000" points="2868.1653,-10923.5107 2872.6462,-10920.685 2867.3812,-10920.0996 2868.1653,-10923.5107"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/opencontainers/go-digest --> +<g id="edge97" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2317.5029,-11195.9989C2626.9012,-11218.7278 3424.5812,-11270.0086 3522,-11202.5998 3582.1434,-11160.9837 3522.6486,-11093.9866 3580,-11048.5998 3604.4471,-11029.2529 3636.0327,-11020.1661 3666.9134,-11016.5608"/> +<polygon fill="#000000" stroke="#000000" points="3667.4784,-11018.2605 3672.2642,-11015.9892 3667.1066,-11014.7803 3667.4784,-11018.2605"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge99" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M2317.6157,-11184.8086C2529.9102,-11181.6948 2966.5936,-11175.2896 3199.7627,-11171.8695"/> +<polygon fill="#000000" stroke="#000000" points="3199.9167,-11173.6175 3204.8904,-11171.7943 3199.8653,-11170.1179 3199.9167,-11173.6175"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/pkg/errors --> +<g id="edge100" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2202.3281,-11168.4554C2226.3709,-11104.3072 2308.8454,-10882.0502 2368,-10695.5998 2419.3106,-10533.8732 2673.5923,-9354.5709 2785,-9226.5998 2913.1823,-9079.3602 2999.944,-9105.4594 3187,-9049.5998 3331.6431,-9006.4058 3406.3733,-9090.6453 3522,-8993.5998 3581.2193,-8943.8971 3519.8639,-8878.1893 3580,-8829.5998 3615.1964,-8801.1615 3665.7177,-8794.6958 3707.3665,-8795.3225"/> +<polygon fill="#000000" stroke="#000000" points="3707.3627,-8797.0728 3712.4,-8795.4331 3707.4397,-8793.5736 3707.3627,-8797.0728"/> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/sirupsen/logrus --> +<g id="edge101" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M2204.6998,-11204.8305C2231.8005,-11256.9091 2316.3579,-11408.2029 2426,-11495.5998 2540.3672,-11586.7632 2619.4963,-11529.4355 2727,-11628.5998 2766.7604,-11665.2759 2744.2054,-11700.0776 2785,-11735.5998 2908.1434,-11842.8277 3024.487,-11739.144 3129,-11864.5998 3222.6343,-11976.9969 3108.1681,-12065.3683 3187,-12188.5998 3215.7462,-12233.5365 3268.5221,-12266.0534 3307.1751,-12285.235"/> +<polygon fill="#000000" stroke="#000000" points="3306.5561,-12286.8804 3311.8163,-12287.5081 3308.0956,-12283.7372 3306.5561,-12286.8804"/> +</g> +<!-- github.com/containers/image/v5/manifest->regexp --> +<g id="edge102" class="edge"> +<title>github.com/containers/image/v5/manifest->regexp</title> +<path fill="none" stroke="#000000" d="M2204.3691,-11168.4663C2234.7454,-11105.2345 2334.562,-10887.8715 2368,-10695.5998 2389.1608,-10573.9235 2358.2528,-1906.8627 2426,-1803.5998 2506.2281,-1681.313 2597.3688,-1738.3225 2727,-1670.5998 2939.1361,-1559.7745 2969.0712,-1489.5492 3187,-1390.5998 3353.7695,-1314.8791 3398.9797,-1294.4803 3580,-1266.5998 3757.0238,-1239.3349 3849.6284,-1147.0473 3983,-1266.5998 4022.0078,-1301.5659 4083.4872,-2125.1148 4094.337,-2274.4789"/> +<polygon fill="#000000" stroke="#000000" points="4092.5944,-2274.6451 4094.7015,-2279.5054 4096.0852,-2274.3919 4092.5944,-2274.6451"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression --> +<g id="node71" class="node"> +<title>github.com/containers/image/v5/pkg/compression</title> +<g id="a_node71"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression" xlink:title="github.com/containers/image/v5/pkg/compression" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2709.5,-12366.5998C2709.5,-12366.5998 2443.5,-12366.5998 2443.5,-12366.5998 2437.5,-12366.5998 2431.5,-12360.5998 2431.5,-12354.5998 2431.5,-12354.5998 2431.5,-12342.5998 2431.5,-12342.5998 2431.5,-12336.5998 2437.5,-12330.5998 2443.5,-12330.5998 2443.5,-12330.5998 2709.5,-12330.5998 2709.5,-12330.5998 2715.5,-12330.5998 2721.5,-12336.5998 2721.5,-12342.5998 2721.5,-12342.5998 2721.5,-12354.5998 2721.5,-12354.5998 2721.5,-12360.5998 2715.5,-12366.5998 2709.5,-12366.5998"/> +<text text-anchor="middle" x="2576.5" y="-12344.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/image/v5/pkg/compression --> +<g id="edge91" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/image/v5/pkg/compression</title> +<path fill="none" stroke="#000000" d="M2201.4846,-11204.8521C2245.5519,-11339.2517 2519.457,-12174.6261 2568.8664,-12325.3183"/> +<polygon fill="#000000" stroke="#000000" points="2567.3078,-12326.1819 2570.5286,-12330.3878 2570.6336,-12325.0914 2567.3078,-12326.1819"/> +</g> +<!-- github.com/containers/image/v5/pkg/strslice --> +<g id="node72" class="node"> +<title>github.com/containers/image/v5/pkg/strslice</title> +<g id="a_node72"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/strslice" xlink:title="github.com/containers/image/v5/pkg/strslice" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3074.5,-17522.5998C3074.5,-17522.5998 2839.5,-17522.5998 2839.5,-17522.5998 2833.5,-17522.5998 2827.5,-17516.5998 2827.5,-17510.5998 2827.5,-17510.5998 2827.5,-17498.5998 2827.5,-17498.5998 2827.5,-17492.5998 2833.5,-17486.5998 2839.5,-17486.5998 2839.5,-17486.5998 3074.5,-17486.5998 3074.5,-17486.5998 3080.5,-17486.5998 3086.5,-17492.5998 3086.5,-17498.5998 3086.5,-17498.5998 3086.5,-17510.5998 3086.5,-17510.5998 3086.5,-17516.5998 3080.5,-17522.5998 3074.5,-17522.5998"/> +<text text-anchor="middle" x="2957" y="-17500.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/strslice</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/image/v5/pkg/strslice --> +<g id="edge92" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/image/v5/pkg/strslice</title> +<path fill="none" stroke="#000000" d="M2196.3889,-11204.755C2206.7975,-11417.7218 2306.2245,-13462.4563 2368,-15116.5998 2371.9059,-15221.1876 2380.169,-16905.5074 2426,-16999.5998 2502.2766,-17156.1981 2617.6598,-17115.0054 2727,-17250.5998 2761.9401,-17293.9295 2750.0281,-17318.2958 2785,-17361.5998 2826.0298,-17412.4049 2887.3743,-17458.1685 2924.6399,-17483.5913"/> +<polygon fill="#000000" stroke="#000000" points="2923.9416,-17485.2322 2929.0622,-17486.5898 2925.9059,-17482.3353 2923.9416,-17485.2322"/> +</g> +<!-- github.com/containers/libtrust --> +<g id="node73" class="node"> +<title>github.com/containers/libtrust</title> +<g id="a_node73"><a xlink:href="https://godoc.org/github.com/containers/libtrust" xlink:title="github.com/containers/libtrust" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3859.5,-16882.5998C3859.5,-16882.5998 3703.5,-16882.5998 3703.5,-16882.5998 3697.5,-16882.5998 3691.5,-16876.5998 3691.5,-16870.5998 3691.5,-16870.5998 3691.5,-16858.5998 3691.5,-16858.5998 3691.5,-16852.5998 3697.5,-16846.5998 3703.5,-16846.5998 3703.5,-16846.5998 3859.5,-16846.5998 3859.5,-16846.5998 3865.5,-16846.5998 3871.5,-16852.5998 3871.5,-16858.5998 3871.5,-16858.5998 3871.5,-16870.5998 3871.5,-16870.5998 3871.5,-16876.5998 3865.5,-16882.5998 3859.5,-16882.5998"/> +<text text-anchor="middle" x="3781.5" y="-16860.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/libtrust</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/libtrust --> +<g id="edge94" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/libtrust</title> +<path fill="none" stroke="#000000" d="M2196.3499,-11204.7565C2206.3084,-11417.7407 2301.63,-13462.6342 2368,-15116.5998 2371.5553,-15205.1982 2365.7299,-16646.5627 2426,-16711.5998 2769.9562,-17082.7615 3484.0615,-16940.9232 3710.7964,-16883.8743"/> +<polygon fill="#000000" stroke="#000000" points="3711.3167,-16885.5479 3715.7352,-16882.6256 3710.4588,-16882.1547 3711.3167,-16885.5479"/> +</g> +<!-- github.com/containers/ocicrypt/spec --> +<g id="node74" class="node"> +<title>github.com/containers/ocicrypt/spec</title> +<g id="a_node74"><a xlink:href="https://godoc.org/github.com/containers/ocicrypt/spec" xlink:title="github.com/containers/ocicrypt/spec" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2671.5,-11311.5998C2671.5,-11311.5998 2481.5,-11311.5998 2481.5,-11311.5998 2475.5,-11311.5998 2469.5,-11305.5998 2469.5,-11299.5998 2469.5,-11299.5998 2469.5,-11287.5998 2469.5,-11287.5998 2469.5,-11281.5998 2475.5,-11275.5998 2481.5,-11275.5998 2481.5,-11275.5998 2671.5,-11275.5998 2671.5,-11275.5998 2677.5,-11275.5998 2683.5,-11281.5998 2683.5,-11287.5998 2683.5,-11287.5998 2683.5,-11299.5998 2683.5,-11299.5998 2683.5,-11305.5998 2677.5,-11311.5998 2671.5,-11311.5998"/> +<text text-anchor="middle" x="2576.5" y="-11289.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/ocicrypt/spec</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/containers/ocicrypt/spec --> +<g id="edge95" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/containers/ocicrypt/spec</title> +<path fill="none" stroke="#000000" d="M2256.0549,-11204.616C2302.4806,-11218.3278 2368.1854,-11237.5166 2426,-11253.5998 2450.6816,-11260.4659 2477.7119,-11267.7296 2501.9704,-11274.1557"/> +<polygon fill="#000000" stroke="#000000" points="2501.8217,-11275.9265 2507.103,-11275.5138 2502.717,-11272.5429 2501.8217,-11275.9265"/> +</g> +<!-- github.com/docker/docker/api/types/versions --> +<g id="node75" class="node"> +<title>github.com/docker/docker/api/types/versions</title> +<g id="a_node75"><a xlink:href="https://godoc.org/github.com/docker/docker/api/types/versions" xlink:title="github.com/docker/docker/api/types/versions" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3900,-5847.5998C3900,-5847.5998 3663,-5847.5998 3663,-5847.5998 3657,-5847.5998 3651,-5841.5998 3651,-5835.5998 3651,-5835.5998 3651,-5823.5998 3651,-5823.5998 3651,-5817.5998 3657,-5811.5998 3663,-5811.5998 3663,-5811.5998 3900,-5811.5998 3900,-5811.5998 3906,-5811.5998 3912,-5817.5998 3912,-5823.5998 3912,-5823.5998 3912,-5835.5998 3912,-5835.5998 3912,-5841.5998 3906,-5847.5998 3900,-5847.5998"/> +<text text-anchor="middle" x="3781.5" y="-5825.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/api/types/versions</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/docker/docker/api/types/versions --> +<g id="edge96" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/docker/docker/api/types/versions</title> +<path fill="none" stroke="#000000" d="M2204.3113,-11168.4562C2234.4987,-11105.1911 2333.7724,-10887.7325 2368,-10695.5998 2444.6902,-10265.1078 2309.1557,-7181.9693 2426,-6760.5998 2510.8796,-6454.5031 2536.8991,-6340.9573 2785,-6142.5998 3043.2689,-5936.113 3439.203,-5864.9801 3645.6466,-5841.1414"/> +<polygon fill="#000000" stroke="#000000" points="3646.0219,-5842.86 3650.7905,-5840.5527 3645.6239,-5839.3826 3646.0219,-5842.86"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go --> +<g id="node76" class="node"> +<title>github.com/opencontainers/image-spec/specs-go</title> +<g id="a_node76"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image-spec/specs-go" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3910,-9541.5998C3910,-9541.5998 3653,-9541.5998 3653,-9541.5998 3647,-9541.5998 3641,-9535.5998 3641,-9529.5998 3641,-9529.5998 3641,-9517.5998 3641,-9517.5998 3641,-9511.5998 3647,-9505.5998 3653,-9505.5998 3653,-9505.5998 3910,-9505.5998 3910,-9505.5998 3916,-9505.5998 3922,-9511.5998 3922,-9517.5998 3922,-9517.5998 3922,-9529.5998 3922,-9529.5998 3922,-9535.5998 3916,-9541.5998 3910,-9541.5998"/> +<text text-anchor="middle" x="3781.5" y="-9519.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->github.com/opencontainers/image-spec/specs-go --> +<g id="edge98" class="edge"> +<title>github.com/containers/image/v5/manifest->github.com/opencontainers/image-spec/specs-go</title> +<path fill="none" stroke="#000000" d="M2317.8182,-11171.4229C2546.8752,-11140.2948 3026.6579,-11061.3828 3129,-10935.5998 3269.6257,-10762.7645 3094.7667,-10137.4315 3187,-9934.5998 3237.6073,-9823.3085 3470.0053,-9602.9668 3580,-9549.5998 3597.407,-9541.1543 3616.6053,-9535.1343 3636.0281,-9530.8882"/> +<polygon fill="#000000" stroke="#000000" points="3636.4385,-9532.5902 3640.97,-9529.8465 3635.7165,-9529.1655 3636.4385,-9532.5902"/> +</g> +<!-- runtime --> +<g id="node77" class="node"> +<title>runtime</title> +<g id="a_node77"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4113.5,-17802.5998C4113.5,-17802.5998 4078.5,-17802.5998 4078.5,-17802.5998 4072.5,-17802.5998 4066.5,-17796.5998 4066.5,-17790.5998 4066.5,-17790.5998 4066.5,-17778.5998 4066.5,-17778.5998 4066.5,-17772.5998 4072.5,-17766.5998 4078.5,-17766.5998 4078.5,-17766.5998 4113.5,-17766.5998 4113.5,-17766.5998 4119.5,-17766.5998 4125.5,-17772.5998 4125.5,-17778.5998 4125.5,-17778.5998 4125.5,-17790.5998 4125.5,-17790.5998 4125.5,-17796.5998 4119.5,-17802.5998 4113.5,-17802.5998"/> +<text text-anchor="middle" x="4096" y="-17780.8998" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/manifest->runtime --> +<g id="edge103" class="edge"> +<title>github.com/containers/image/v5/manifest->runtime</title> +<path fill="none" stroke="#000000" d="M2196.4046,-11204.7544C2206.9937,-11417.7145 2308.0677,-13462.3885 2368,-15116.5998 2376.1699,-15342.0995 2376.4136,-16926.468 2426,-17146.5998 2498.9868,-17470.6144 2596.5794,-17525.1445 2727,-17830.5998 2910.4101,-18260.1609 3012.2884,-18349.3384 3129,-18801.5998 3157.6673,-18912.6868 3120.7613,-19220.9272 3187,-19314.5998 3292.028,-19463.127 3851.5795,-19631.3768 3983,-19505.5998 4036.0708,-19454.808 4034.3785,-18257.7604 4041,-18184.5998 4054.0027,-18040.9334 4081.6414,-17869.7647 4091.9831,-17808.1467"/> +<polygon fill="#000000" stroke="#000000" points="4093.7669,-17808.0919 4092.8714,-17802.8707 4090.3155,-17807.5108 4093.7669,-17808.0919"/> +</g> +<!-- github.com/containers/image/v5/pkg/blobinfocache/none->github.com/containers/image/v5/types --> +<g id="edge106" class="edge"> +<title>github.com/containers/image/v5/pkg/blobinfocache/none->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M2295.5294,-10680.6285C2405.4873,-10702.9824 2585.3326,-10747.1282 2727,-10818.5998 2755.8879,-10833.1738 2755.7086,-10848.8548 2785,-10862.5998 2803.0381,-10871.0642 2823.0032,-10877.8089 2842.7543,-10883.1615"/> +<polygon fill="#000000" stroke="#000000" points="2842.4882,-10884.9014 2847.7698,-10884.4925 2843.386,-10881.5185 2842.4882,-10884.9014"/> +</g> +<!-- github.com/containers/image/v5/pkg/blobinfocache/none->github.com/opencontainers/go-digest --> +<g id="edge107" class="edge"> +<title>github.com/containers/image/v5/pkg/blobinfocache/none->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2359.1467,-10673.1056C2464.3601,-10680.4589 2603.9119,-10691.3273 2727,-10704.5998 2906.2918,-10723.9328 2985.3475,-10650.5894 3129,-10759.5998 3180.6956,-10798.8289 3136.9441,-10852.299 3187,-10893.5998 3304.6121,-10990.6409 3373.7214,-10932.0608 3522,-10967.5998 3547.8926,-10973.8057 3554.0424,-10976.6719 3580,-10982.5998 3612.3443,-10989.9863 3647.7886,-10997.2863 3679.8092,-11003.5873"/> +<polygon fill="#000000" stroke="#000000" points="3679.6744,-11005.3442 3684.9179,-11004.5899 3680.3485,-11001.9097 3679.6744,-11005.3442"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->encoding/base64 --> +<g id="edge122" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->encoding/base64</title> +<path fill="none" stroke="#000000" d="M1529.5775,-15988.7881C1548.449,-16083.0788 1639.0822,-16515.5515 1732,-16610.5998 2388.664,-17282.32 3739.9327,-17000.1237 4037.285,-16927.6718"/> +<polygon fill="#000000" stroke="#000000" points="4037.9915,-16929.3007 4042.4325,-16926.4128 4037.1599,-16925.9009 4037.9915,-16929.3007"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->encoding/json --> +<g id="edge123" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->encoding/json</title> +<path fill="none" stroke="#000000" d="M1527.2953,-15988.6831C1534.2668,-16076.1234 1573.1513,-16461.5427 1732,-16722.5998 2062.9156,-17266.4372 2255.7123,-17339.8786 2785,-17693.5998 2948.6778,-17802.9851 2995.012,-17836.0548 3187,-17879.5998 3357.354,-17918.238 3405.3227,-17883.4887 3580,-17884.5998 3759.1075,-17885.7391 3845.8865,-17999.8416 3983,-17884.5998 4032.3001,-17843.1639 4082.5883,-17370.9993 4093.7274,-17260.591"/> +<polygon fill="#000000" stroke="#000000" points="4095.4692,-17260.7601 4094.2281,-17255.6101 4091.9867,-17260.41 4095.4692,-17260.7601"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->fmt --> +<g id="edge124" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->fmt</title> +<path fill="none" stroke="#000000" d="M1526.121,-15952.1931C1528.2689,-15637.5838 1560.3923,-11389.018 1732,-10870.5998 1846.8228,-10523.7259 1830.1155,-10191.5998 2195.5,-10191.5998 2195.5,-10191.5998 2195.5,-10191.5998 2576.5,-10191.5998 2979.3565,-10191.5998 2844.4167,-9733.567 3187,-9521.5998 3318.6055,-9440.1713 3417.5451,-9540.7917 3522,-9426.5998 3615.4273,-9324.4635 3479.7411,-9216.039 3580,-9120.5998 3710.977,-8995.9192 3843.0937,-9179.1707 3983,-9064.5998 4031.3803,-9024.9807 4076.6048,-8832.1019 4091.1479,-8764.8137"/> +<polygon fill="#000000" stroke="#000000" points="4092.8824,-8765.0714 4092.2215,-8759.8154 4089.4604,-8764.3363 4092.8824,-8765.0714"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->io/ioutil --> +<g id="edge132" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1527.109,-15988.7095C1532.8013,-16065.3515 1565.3738,-16366.0774 1732,-16518.5998 1968.1537,-16734.7647 2108.7901,-16668.3134 2426,-16711.5998 2558.5494,-16729.6875 2593.5835,-16721.425 2727,-16711.5998 2797.3483,-16706.4192 3926.2958,-16567.5563 3983,-16525.5998 4031.2628,-16489.8893 4013.9639,-16457.2057 4041,-16403.5998 4055.0881,-16375.6666 4072.1785,-16343.9797 4083.5315,-16323.2165"/> +<polygon fill="#000000" stroke="#000000" points="4085.1385,-16323.9254 4086.0051,-16318.6993 4082.0686,-16322.2443 4085.1385,-16323.9254"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->strings --> +<g id="edge135" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->strings</title> +<path fill="none" stroke="#000000" d="M1526.2952,-15952.5325C1532.2242,-15591.8263 1626.6013,-9947.4987 1732,-9622.5998 1972.1394,-8882.3535 2231.8013,-8770.9597 2785,-8223.5998 2928.5826,-8081.5327 3027.4104,-8101.1809 3129,-7926.5998 3195.7661,-7811.8627 3090.6729,-7724.9421 3187,-7633.5998 3296.1421,-7530.1057 3402.5957,-7677.0636 3522,-7585.5998 3573.8416,-7545.8891 3528.0743,-7490.2005 3580,-7450.5998 3723.7885,-7340.9408 3842.7571,-7508.7584 3983,-7394.5998 4030.5845,-7355.8658 4076.1498,-7167.3175 4090.9802,-7100.8488"/> +<polygon fill="#000000" stroke="#000000" points="4092.702,-7101.1675 4092.0762,-7095.9072 4089.285,-7100.4096 4092.702,-7101.1675"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/containers/image/v5/types --> +<g id="edge126" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M1527.3977,-15952.5127C1537.9473,-15818.1211 1608.0477,-14961.2735 1732,-14274.5998 1966.0416,-12978.051 1484.9064,-12314.6395 2426,-11392.5998 2523.8278,-11296.7527 2633.4027,-11426.5822 2727,-11326.5998 2844.9585,-11200.5943 2667.8404,-11069.3485 2785,-10942.5998 2799.5455,-10926.8638 2818.9703,-10916.7388 2839.5257,-10910.3339"/> +<polygon fill="#000000" stroke="#000000" points="2840.1919,-10911.9618 2844.4893,-10908.8642 2839.1981,-10908.6058 2840.1919,-10911.9618"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/pkg/errors --> +<g id="edge130" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M1526.3321,-15952.5528C1532.6953,-15608.7261 1629.5326,-10463.9705 1732,-10169.5998 1963.6182,-9504.2016 2134.1115,-9254.3143 2785,-8984.5998 3110.5682,-8849.6912 3537.1531,-8815.0817 3707.0281,-8806.4128"/> +<polygon fill="#000000" stroke="#000000" points="3707.319,-8808.1505 3712.2248,-8806.1517 3707.1433,-8804.6549 3707.319,-8808.1505"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/sirupsen/logrus --> +<g id="edge131" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M1528.0912,-15952.4036C1552.9554,-15743.5434 1808.7882,-13786.3616 2785,-12664.5998 2899.5182,-12533.0073 2982.5303,-12570.3501 3129,-12475.5998 3204.4185,-12426.8121 3287.3825,-12360.787 3328.4654,-12327.1839"/> +<polygon fill="#000000" stroke="#000000" points="3329.7898,-12328.3612 3332.548,-12323.8385 3327.5714,-12325.654 3329.7898,-12328.3612"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->os --> +<g id="edge133" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->os</title> +<path fill="none" stroke="#000000" d="M1529.263,-15988.7038C1546.7104,-16084.7001 1631.9417,-16543.6622 1732,-16911.5998 1797.207,-17151.3812 2320.7837,-18812.4852 2426,-19037.5998 2553.4072,-19310.193 2563.5485,-19407.8871 2785,-19611.5998 2986.027,-19796.5241 3782.4912,-20072.0859 3983,-19886.5998 4043.8673,-19830.2928 4030.7868,-19224.8859 4041,-19142.5998 4054.9798,-19029.9673 4080.7143,-18896.652 4091.2816,-18843.824"/> +<polygon fill="#000000" stroke="#000000" points="4093.0479,-18843.9166 4092.3156,-18838.67 4089.6162,-18843.2281 4093.0479,-18843.9166"/> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->path/filepath --> +<g id="edge134" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->path/filepath</title> +<path fill="none" stroke="#000000" d="M1528.0538,-15988.8345C1541.4939,-16106.7859 1619.8474,-16773.7912 1732,-17308.5998 2007.6102,-18622.8695 1744.5982,-19291.5828 2785,-20140.5998 2993.3266,-20310.6041 3781.7262,-20488.8981 3983,-20310.5998 4053.3176,-20248.3091 4088.7485,-19541.0273 4094.9914,-19403.6409"/> +<polygon fill="#000000" stroke="#000000" points="4096.7405,-19403.7002 4095.2178,-19398.6263 4093.244,-19403.5423 4096.7405,-19403.7002"/> +</g> +<!-- github.com/containers/image/v5/internal/pkg/keyctl --> +<g id="node69" class="node"> +<title>github.com/containers/image/v5/internal/pkg/keyctl</title> +<g id="a_node69"><a xlink:href="https://godoc.org/github.com/containers/image/v5/internal/pkg/keyctl" xlink:title="github.com/containers/image/v5/internal/pkg/keyctl" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3492,-16050.5998C3492,-16050.5998 3217,-16050.5998 3217,-16050.5998 3211,-16050.5998 3205,-16044.5998 3205,-16038.5998 3205,-16038.5998 3205,-16026.5998 3205,-16026.5998 3205,-16020.5998 3211,-16014.5998 3217,-16014.5998 3217,-16014.5998 3492,-16014.5998 3492,-16014.5998 3498,-16014.5998 3504,-16020.5998 3504,-16026.5998 3504,-16026.5998 3504,-16038.5998 3504,-16038.5998 3504,-16044.5998 3498,-16050.5998 3492,-16050.5998"/> +<text text-anchor="middle" x="3354.5" y="-16028.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/internal/pkg/keyctl</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/containers/image/v5/internal/pkg/keyctl --> +<g id="edge125" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/containers/image/v5/internal/pkg/keyctl</title> +<path fill="none" stroke="#000000" d="M1527.1584,-15988.7618C1532.9545,-16062.8322 1565.5981,-16343.862 1732,-16464.5998 1899.2642,-16585.9633 1988.8447,-16501.5998 2195.5,-16501.5998 2195.5,-16501.5998 2195.5,-16501.5998 2576.5,-16501.5998 2822.6056,-16501.5998 2922.9115,-16599.1193 3129,-16464.5998 3280.5173,-16365.7004 3336.2558,-16130.5425 3350.4987,-16055.8162"/> +<polygon fill="#000000" stroke="#000000" points="3352.2532,-16055.9539 3351.4532,-16050.7172 3348.813,-16055.3098 3352.2532,-16055.9539"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client --> +<g id="node83" class="node"> +<title>github.com/docker/docker-credential-helpers/client</title> +<g id="a_node83"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/client" xlink:title="github.com/docker/docker-credential-helpers/client" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3490,-15454.5998C3490,-15454.5998 3219,-15454.5998 3219,-15454.5998 3213,-15454.5998 3207,-15448.5998 3207,-15442.5998 3207,-15442.5998 3207,-15430.5998 3207,-15430.5998 3207,-15424.5998 3213,-15418.5998 3219,-15418.5998 3219,-15418.5998 3490,-15418.5998 3490,-15418.5998 3496,-15418.5998 3502,-15424.5998 3502,-15430.5998 3502,-15430.5998 3502,-15442.5998 3502,-15442.5998 3502,-15448.5998 3496,-15454.5998 3490,-15454.5998"/> +<text text-anchor="middle" x="3354.5" y="-15432.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker-credential-helpers/client</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker-credential-helpers/client --> +<g id="edge127" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker-credential-helpers/client</title> +<path fill="none" stroke="#000000" d="M1674.2492,-15970.5998C1809.9152,-15970.5998 2016.2112,-15970.5998 2195.5,-15970.5998 2195.5,-15970.5998 2195.5,-15970.5998 2576.5,-15970.5998 2839.8318,-15970.5998 2921.4931,-15918.7249 3129,-15756.5998 3238.913,-15670.7248 3317.5229,-15516.8768 3344.3796,-15459.2324"/> +<polygon fill="#000000" stroke="#000000" points="3346.0103,-15459.8753 3346.5214,-15454.6026 3342.8338,-15458.4057 3346.0103,-15459.8753"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials --> +<g id="node84" class="node"> +<title>github.com/docker/docker-credential-helpers/credentials</title> +<g id="a_node84"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" xlink:title="github.com/docker/docker-credential-helpers/credentials" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3931,-15519.5998C3931,-15519.5998 3632,-15519.5998 3632,-15519.5998 3626,-15519.5998 3620,-15513.5998 3620,-15507.5998 3620,-15507.5998 3620,-15495.5998 3620,-15495.5998 3620,-15489.5998 3626,-15483.5998 3632,-15483.5998 3632,-15483.5998 3931,-15483.5998 3931,-15483.5998 3937,-15483.5998 3943,-15489.5998 3943,-15495.5998 3943,-15495.5998 3943,-15507.5998 3943,-15507.5998 3943,-15513.5998 3937,-15519.5998 3931,-15519.5998"/> +<text text-anchor="middle" x="3781.5" y="-15497.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker-credential-helpers/credentials</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker-credential-helpers/credentials --> +<g id="edge128" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker-credential-helpers/credentials</title> +<path fill="none" stroke="#000000" d="M1538.3868,-15988.8136C1599.0007,-16075.5563 1874.4511,-16442.5998 2195.5,-16442.5998 2195.5,-16442.5998 2195.5,-16442.5998 2576.5,-16442.5998 2653.7824,-16442.5998 3587.9955,-15663.5895 3755.7788,-15523.1568"/> +<polygon fill="#000000" stroke="#000000" points="3757.1193,-15524.3169 3759.8299,-15519.7654 3754.8726,-15521.6331 3757.1193,-15524.3169"/> +</g> +<!-- github.com/docker/docker/pkg/homedir --> +<g id="node85" class="node"> +<title>github.com/docker/docker/pkg/homedir</title> +<g id="a_node85"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/homedir" xlink:title="github.com/docker/docker/pkg/homedir" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1953,-15546.5998C1953,-15546.5998 1744,-15546.5998 1744,-15546.5998 1738,-15546.5998 1732,-15540.5998 1732,-15534.5998 1732,-15534.5998 1732,-15522.5998 1732,-15522.5998 1732,-15516.5998 1738,-15510.5998 1744,-15510.5998 1744,-15510.5998 1953,-15510.5998 1953,-15510.5998 1959,-15510.5998 1965,-15516.5998 1965,-15522.5998 1965,-15522.5998 1965,-15534.5998 1965,-15534.5998 1965,-15540.5998 1959,-15546.5998 1953,-15546.5998"/> +<text text-anchor="middle" x="1848.5" y="-15524.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/homedir</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker/pkg/homedir --> +<g id="edge129" class="edge"> +<title>github.com/containers/image/v5/pkg/docker/config->github.com/docker/docker/pkg/homedir</title> +<path fill="none" stroke="#000000" d="M1539.2436,-15952.4489C1590.4934,-15882.2089 1775.7401,-15628.3204 1832.1107,-15551.0621"/> +<polygon fill="#000000" stroke="#000000" points="1833.7905,-15551.7288 1835.3239,-15546.6582 1830.9631,-15549.6658 1833.7905,-15551.7288"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->fmt --> +<g id="edge137" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->fmt</title> +<path fill="none" stroke="#000000" d="M2647.6225,-8714.5606C2784.911,-8680.0854 3082.1395,-8607.221 3187,-8595.5998 3363.3977,-8576.0505 3818.2486,-8594.6053 3983,-8660.5998 4017.5939,-8674.4571 4051.5613,-8701.061 4073.0755,-8719.9968"/> +<polygon fill="#000000" stroke="#000000" points="4072.046,-8721.4231 4076.9451,-8723.4384 4074.3721,-8718.8079 4072.046,-8721.4231"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->github.com/BurntSushi/toml --> +<g id="edge138" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->github.com/BurntSushi/toml</title> +<path fill="none" stroke="#000000" d="M2578.0479,-8714.4861C2597.0003,-8492.7141 2783.6428,-6309.0763 2785,-6307.5998 2889.9799,-6193.399 3021.2927,-6360.232 3129,-6248.5998 3262.8983,-6109.8223 3106.4459,-5993.8111 3187,-5818.5998 3280.4931,-5615.2453 3412.4826,-5637.7919 3522,-5442.5998 3565.4105,-5365.2296 3534.3323,-5327.6595 3580,-5251.5998 3626.3237,-5174.4476 3708.7202,-5105.3401 3752.4176,-5071.8796"/> +<polygon fill="#000000" stroke="#000000" points="3753.605,-5073.1751 3756.523,-5068.7537 3751.4847,-5070.3904 3753.605,-5073.1751"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->io/ioutil --> +<g id="edge143" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2577.8559,-8750.6796C2590.3335,-8917.8237 2685.6554,-10211.1973 2727,-11261.5998 2733.5868,-11428.9457 2729.6172,-14117.5467 2785,-14275.5998 2868.0163,-14512.5144 3043.1525,-14487.6963 3129,-14723.5998 3180.0391,-14863.852 3086.822,-15954.9651 3187,-16065.5998 3237.478,-16121.3468 3448.2418,-16098.9207 3522,-16113.5998 3729.4264,-16154.8812 3788.1504,-16149.3592 3983,-16231.5998 4013.1775,-16244.3369 4044.7719,-16264.2736 4066.8553,-16279.4482"/> +<polygon fill="#000000" stroke="#000000" points="4066.0349,-16281.0086 4071.1419,-16282.416 4068.0272,-16278.131 4066.0349,-16281.0086"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->strings --> +<g id="edge147" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->strings</title> +<path fill="none" stroke="#000000" d="M2605.1108,-8714.441C2700.3191,-8652.1811 3007.3002,-8436.2922 3129,-8167.5998 3198.7009,-8013.7121 3086.8425,-7545.6444 3187,-7409.5998 3281.9739,-7280.596 3382.4635,-7355.2864 3522,-7276.5998 3550.1837,-7260.7066 3550.3943,-7245.6543 3580,-7232.5998 3746.626,-7159.127 3815.3947,-7229.8103 3983,-7158.5998 4017.0896,-7144.1161 4050.912,-7117.893 4072.5394,-7099.2079"/> +<polygon fill="#000000" stroke="#000000" points="4073.8157,-7100.4169 4076.4331,-7095.8112 4071.5148,-7097.7794 4073.8157,-7100.4169"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->sync --> +<g id="edge148" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->sync</title> +<path fill="none" stroke="#000000" d="M2578.1386,-8714.3887C2590.4632,-8577.2244 2669.7107,-7691.9421 2727,-6971.5998 2754.8408,-6621.5356 2712.6111,-6527.2274 2785,-6183.5998 2879.0317,-5737.2345 3046.1005,-5665.166 3129,-5216.5998 3153.6923,-5082.9908 3101.3244,-2877.0548 3187,-2771.5998 3243.1411,-2702.4978 3913.5562,-2623.8821 3983,-2679.5998 4060.7906,-2742.0146 4088.3866,-3074.1201 4094.5626,-3165.3802"/> +<polygon fill="#000000" stroke="#000000" points="4092.8279,-3165.6706 4094.9064,-3170.5432 4096.3202,-3165.438 4092.8279,-3165.6706"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->github.com/containers/image/v5/docker/reference --> +<g id="edge139" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M2577.685,-8714.4708C2588.9578,-8544.1897 2679.1855,-7228.7641 2785,-7093.5998 2886.0441,-6964.5292 3018.2224,-7081.4198 3129,-6960.5998 3191.5471,-6892.3826 3130.2829,-6833.736 3187,-6760.5998 3215.6271,-6723.6854 3262.2046,-6697.7636 3298.9851,-6681.7301"/> +<polygon fill="#000000" stroke="#000000" points="3299.973,-6683.2105 3303.8774,-6679.6302 3298.5925,-6679.9943 3299.973,-6683.2105"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->github.com/containers/image/v5/types --> +<g id="edge140" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M2577.678,-8750.8797C2590.8242,-8953.7309 2710.9692,-10778.1821 2785,-10862.5998 2799.2224,-10878.8177 2818.6195,-10889.1424 2839.2725,-10895.5883"/> +<polygon fill="#000000" stroke="#000000" points="2838.9708,-10897.3239 2844.2619,-10897.0648 2839.9641,-10893.9678 2838.9708,-10897.3239"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->github.com/pkg/errors --> +<g id="edge141" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2727.3727,-8735.4207C2914.1742,-8739.7337 3241.9654,-8749.9832 3522,-8772.5998 3584.5247,-8777.6495 3655.2001,-8786.2863 3706.9874,-8793.1629"/> +<polygon fill="#000000" stroke="#000000" points="3707.0678,-8794.939 3712.2552,-8793.865 3707.5302,-8791.4697 3707.0678,-8794.939"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->github.com/sirupsen/logrus --> +<g id="edge142" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M2577.3134,-8750.9294C2587.9919,-8990.1723 2701.5672,-11493.091 2785,-11621.5998 2879.4153,-11767.0246 3028.9688,-11663.9793 3129,-11805.5998 3228.3258,-11946.2216 3096.1799,-12042.3404 3187,-12188.5998 3215.0507,-12233.7734 3267.6847,-12266.1609 3306.4931,-12285.2468"/> +<polygon fill="#000000" stroke="#000000" points="3305.8923,-12286.9004 3311.1547,-12287.5084 3307.42,-12283.7514 3305.8923,-12286.9004"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->os --> +<g id="edge144" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->os</title> +<path fill="none" stroke="#000000" d="M2577.895,-8750.6781C2590.7258,-8917.8088 2688.6092,-10211.0852 2727,-11261.5998 2735.6419,-11498.075 2734.7417,-15289.3655 2785,-15520.5998 2865.6436,-15891.6342 3046.9237,-15931.8798 3129,-16302.5998 3161.636,-16450.0091 3087.2696,-18906.2488 3187,-19019.5998 3304.166,-19152.7677 3834.535,-19175.6551 3983,-19078.5998 4065.1091,-19024.923 4087.9225,-18896.9389 4093.9508,-18844.0689"/> +<polygon fill="#000000" stroke="#000000" points="4095.7127,-18844.0536 4094.5142,-18838.8935 4092.2332,-18843.6748 4095.7127,-18844.0536"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->path/filepath --> +<g id="edge145" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->path/filepath</title> +<path fill="none" stroke="#000000" d="M2577.9034,-8750.6778C2590.8098,-8917.8058 2689.2414,-10211.0623 2727,-11261.5998 2731.6618,-11391.3038 2739.8565,-15811.9161 2785,-15933.5998 2867.7356,-16156.6127 3045.3005,-16120.9468 3129,-16343.5998 3182.8297,-16486.7946 3093.8108,-18975.2814 3187,-19096.5998 3281.4236,-19219.5252 3403.7288,-19093.408 3522,-19193.5998 3567.5038,-19232.1478 3531.6548,-19279.6816 3580,-19314.5998 3598.9332,-19328.2747 3924.2549,-19362.9506 4048.3942,-19375.7535"/> +<polygon fill="#000000" stroke="#000000" points="4048.2861,-19377.5015 4053.4391,-19376.2732 4048.6448,-19374.0199 4048.2861,-19377.5015"/> +</g> +<!-- github.com/containers/image/v5/pkg/sysregistriesv2->regexp --> +<g id="edge146" class="edge"> +<title>github.com/containers/image/v5/pkg/sysregistriesv2->regexp</title> +<path fill="none" stroke="#000000" d="M2578.4359,-8714.4091C2592.9383,-8577.3946 2685.1648,-7693.0046 2727,-6971.5998 2775.7825,-6130.3965 2721.4783,-5916.8187 2785,-5076.5998 2785,-5076.5998 3187,-1751.5998 3187,-1751.5998 3317.1343,-1631.7421 3846.1032,-1624.5281 3983,-1736.5998 4068.3526,-1806.4745 4090.6545,-2177.2095 4095.0685,-2274.3066"/> +<polygon fill="#000000" stroke="#000000" points="4093.3296,-2274.599 4095.2999,-2279.5163 4096.8262,-2274.4435 4093.3296,-2274.599"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->crypto/tls --> +<g id="edge149" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->crypto/tls</title> +<path fill="none" stroke="#000000" d="M2579.2997,-16696.7709C2595.9113,-16805.2135 2682.324,-17378.1436 2727,-17848.5998 2803.4743,-18653.9049 2607.8462,-19044.8472 3187,-19609.5998 3316.8768,-19736.2471 3846.6384,-19910.2368 3983,-19790.5998 4057.4666,-19725.2664 4007.0562,-19441.6673 4041,-19348.5998 4049.5652,-19325.1156 4065.3881,-19301.403 4077.8711,-19284.8426"/> +<polygon fill="#000000" stroke="#000000" points="4079.3216,-19285.8266 4080.9663,-19280.7909 4076.5403,-19283.7018 4079.3216,-19285.8266"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->io/ioutil --> +<g id="edge154" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2708.0855,-16660.5795C3052.5294,-16613.2215 3956.0283,-16487.7166 3983,-16470.5998 4011.8119,-16452.3152 4061.2217,-16365.3384 4083.935,-16323.365"/> +<polygon fill="#000000" stroke="#000000" points="4085.6073,-16323.9508 4086.4395,-16318.7192 4082.5264,-16322.29 4085.6073,-16323.9508"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->strings --> +<g id="edge159" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->strings</title> +<path fill="none" stroke="#000000" d="M2578.8411,-16660.5491C2595.2322,-16533.2144 2693.1331,-15756.8559 2727,-15121.5998 2760.7596,-14488.3552 2679.2326,-10039.8611 2785,-9414.5998 2864.8649,-8942.4654 3035.9067,-8863.305 3129,-8393.5998 3166.7994,-8202.8819 3077.1115,-7680.9951 3187,-7520.5998 3279.8272,-7385.1077 3377.5242,-7442.7135 3522,-7364.5998 3548.6885,-7350.1701 3552.2007,-7340.7528 3580,-7328.5998 3750.2253,-7254.1827 3827.6259,-7319.4499 3983,-7217.5998 4029.5598,-7187.0792 4066.062,-7131.1945 4083.9423,-7100.0585"/> +<polygon fill="#000000" stroke="#000000" points="4085.4982,-7100.8625 4086.4442,-7095.6502 4082.4542,-7099.1348 4085.4982,-7100.8625"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->time --> +<g id="edge160" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->time</title> +<path fill="none" stroke="#000000" d="M2578.7047,-16660.5412C2594.162,-16533.152 2686.8352,-15756.4889 2727,-15121.5998 2733.7832,-15014.3769 2730.4187,-13281.1397 2785,-13188.5998 3099.4341,-12655.4923 3654.0672,-13002.8856 3983,-12478.5998 4063.8358,-12349.7558 4024.4635,-11947.8009 4041,-11796.5998 4057.177,-11648.6856 4083.0845,-11471.7016 4092.4649,-11409.0085"/> +<polygon fill="#000000" stroke="#000000" points="4094.2057,-11409.1995 4093.2162,-11403.9953 4090.7444,-11408.6807 4094.2057,-11409.1995"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->github.com/docker/go-connections/tlsconfig --> +<g id="edge151" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->github.com/docker/go-connections/tlsconfig</title> +<path fill="none" stroke="#000000" d="M2578.981,-16696.7038C2591.9924,-16788.8174 2656.69,-17213.7317 2785,-17537.5998 2899.0716,-17825.5288 3022.1904,-17852.8986 3129,-18143.5998 3176.588,-18273.1189 3109.644,-18334.3376 3187,-18448.5998 3214.6981,-18489.5125 3263.8093,-18518.1314 3301.7168,-18535.4104"/> +<polygon fill="#000000" stroke="#000000" points="3301.0039,-18537.0085 3306.2819,-18537.4614 3302.4383,-18533.8159 3301.0039,-18537.0085"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->github.com/pkg/errors --> +<g id="edge152" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2578.7991,-16660.5468C2594.9027,-16533.1963 2691.1941,-15756.7496 2727,-15121.5998 2738.149,-14923.8321 2733.8727,-11747.9696 2785,-11556.5998 2866.4387,-11251.7741 3044.689,-11239.6437 3129,-10935.5998 3178.0197,-10758.8239 3072.0325,-9428.5513 3187,-9285.5998 3283.3356,-9165.8154 3417.1311,-9311.9891 3522,-9199.5998 3619.1166,-9095.5188 3489.9001,-8994.811 3580,-8884.5998 3611.365,-8846.2338 3663.6381,-8825.912 3707.0824,-8815.2191"/> +<polygon fill="#000000" stroke="#000000" points="3707.6098,-8816.8923 3712.0643,-8814.0252 3706.7941,-8813.4887 3707.6098,-8816.8923"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->github.com/sirupsen/logrus --> +<g id="edge153" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M2578.7359,-16660.5431C2594.4063,-16533.1672 2688.2732,-15756.5782 2727,-15121.5998 2734.7033,-14995.2935 2719.5508,-12952.9005 2785,-12844.5998 2875.5599,-12694.7476 3015.5691,-12785.9798 3129,-12652.5998 3178.0021,-12594.9797 3148.1691,-12557.5109 3187,-12492.5998 3227.3704,-12425.1151 3294.0373,-12359.8674 3329.8808,-12327.2249"/> +<polygon fill="#000000" stroke="#000000" points="3331.1434,-12328.4424 3333.6733,-12323.7881 3328.7932,-12325.8488 3331.1434,-12328.4424"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->net/http --> +<g id="edge156" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->net/http</title> +<path fill="none" stroke="#000000" d="M2578.8713,-16660.5507C2595.469,-16533.2267 2694.5272,-15756.9287 2727,-15121.5998 2840.9462,-12892.2505 2657.9615,-7305.2414 2785,-5076.5998 2790.8898,-4973.2745 3142.2869,-1465.9355 3187,-1372.5998 3277.9706,-1182.7047 3427.9631,-1225.9953 3522,-1037.5998 3604.1912,-872.9364 3446.6735,-754.4599 3580,-627.5998 3649.1328,-561.82 3958.1035,-572.7911 4061.8795,-578.4764"/> +<polygon fill="#000000" stroke="#000000" points="4061.868,-580.2284 4066.9577,-578.7594 4062.0628,-576.7338 4061.868,-580.2284"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->os --> +<g id="edge157" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->os</title> +<path fill="none" stroke="#000000" d="M2578.969,-16696.9047C2600.2535,-16854.5467 2752.7436,-17980.7541 2785,-18128.5998 2919.628,-18745.6604 2795.5006,-18996.0025 3187,-19491.5998 3293.3765,-19626.2613 3381.9279,-19584.4561 3522,-19683.5998 3549.7796,-19703.2624 3548.0967,-19721.7466 3580,-19733.5998 3747.8974,-19795.9796 3850.3084,-19853.9067 3983,-19733.5998 4031.8818,-19689.2804 4032.2175,-19207.9948 4041,-19142.5998 4056.1069,-19030.1129 4081.2062,-18896.7156 4091.4427,-18843.8448"/> +<polygon fill="#000000" stroke="#000000" points="4093.2091,-18843.9285 4092.4438,-18838.6866 4089.7732,-18843.2616 4093.2091,-18843.9285"/> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->path/filepath --> +<g id="edge158" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->path/filepath</title> +<path fill="none" stroke="#000000" d="M2579.4378,-16696.7585C2596.845,-16805.1296 2687.0571,-17377.7181 2727,-17848.5998 2740.1873,-18004.0635 2727.2402,-19106.663 2785,-19251.5998 2947.0132,-19658.14 3147.162,-19767.9901 3580,-19832.5998 3757.1484,-19859.0428 3841.8236,-19942.827 3983,-19832.5998 4052.5518,-19778.2954 4085.6291,-19488.2365 4093.8841,-19403.7225"/> +<polygon fill="#000000" stroke="#000000" points="4095.6314,-19403.8334 4094.37,-19398.6884 4092.1476,-19403.4971 4095.6314,-19403.8334"/> +</g> +<!-- github.com/docker/go-connections/sockets --> +<g id="node86" class="node"> +<title>github.com/docker/go-connections/sockets</title> +<g id="a_node86"><a xlink:href="https://godoc.org/github.com/docker/go-connections/sockets" xlink:title="github.com/docker/go-connections/sockets" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3069.5,-9464.5998C3069.5,-9464.5998 2844.5,-9464.5998 2844.5,-9464.5998 2838.5,-9464.5998 2832.5,-9458.5998 2832.5,-9452.5998 2832.5,-9452.5998 2832.5,-9440.5998 2832.5,-9440.5998 2832.5,-9434.5998 2838.5,-9428.5998 2844.5,-9428.5998 2844.5,-9428.5998 3069.5,-9428.5998 3069.5,-9428.5998 3075.5,-9428.5998 3081.5,-9434.5998 3081.5,-9440.5998 3081.5,-9440.5998 3081.5,-9452.5998 3081.5,-9452.5998 3081.5,-9458.5998 3075.5,-9464.5998 3069.5,-9464.5998"/> +<text text-anchor="middle" x="2957" y="-9442.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go-connections/sockets</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->github.com/docker/go-connections/sockets --> +<g id="edge150" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->github.com/docker/go-connections/sockets</title> +<path fill="none" stroke="#000000" d="M2578.8172,-16660.5478C2595.0443,-16533.2042 2692.0277,-15756.796 2727,-15121.5998 2830.8743,-13234.948 2658.6416,-12755.8792 2785,-10870.5998 2823.3116,-10298.9864 2931.6742,-9604.7148 2953.2403,-9469.8904"/> +<polygon fill="#000000" stroke="#000000" points="2955.0235,-9469.8221 2954.0866,-9464.6082 2951.5676,-9469.2683 2955.0235,-9469.8221"/> +</g> +<!-- net --> +<g id="node87" class="node"> +<title>net</title> +<g id="a_node87"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-17190.5998C4111,-17190.5998 4081,-17190.5998 4081,-17190.5998 4075,-17190.5998 4069,-17184.5998 4069,-17178.5998 4069,-17178.5998 4069,-17166.5998 4069,-17166.5998 4069,-17160.5998 4075,-17154.5998 4081,-17154.5998 4081,-17154.5998 4111,-17154.5998 4111,-17154.5998 4117,-17154.5998 4123,-17160.5998 4123,-17166.5998 4123,-17166.5998 4123,-17178.5998 4123,-17178.5998 4123,-17184.5998 4117,-17190.5998 4111,-17190.5998"/> +<text text-anchor="middle" x="4096" y="-17168.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/tlsclientconfig->net --> +<g id="edge155" class="edge"> +<title>github.com/containers/image/v5/pkg/tlsclientconfig->net</title> +<path fill="none" stroke="#000000" d="M2603.2012,-16696.8037C2715.5387,-16772.1851 3163.2476,-17060.5923 3580,-17160.5998 3758.4104,-17203.4127 3981.5207,-17185.4168 4063.6951,-17176.4965"/> +<polygon fill="#000000" stroke="#000000" points="4064.1238,-17178.2099 4068.902,-17175.9228 4063.7404,-17174.731 4064.1238,-17178.2099"/> +</g> +<!-- github.com/containers/image/v5/transports->fmt --> +<g id="edge161" class="edge"> +<title>github.com/containers/image/v5/transports->fmt</title> +<path fill="none" stroke="#000000" d="M2577.4411,-2552.0322C2592.2816,-2842.616 2777.7513,-6471.5908 2785,-6482.5998 2878.7846,-6625.0361 3035.5892,-6509.9181 3129,-6652.5998 3277.6655,-6879.6811 3024.0823,-7655.5165 3187,-7872.5998 3281.4895,-7998.5045 3405.3851,-7881.8586 3522,-7987.5998 3567.9683,-8029.2818 3530.2368,-8077.531 3580,-8114.5998 3725.0196,-8222.6256 3851.5219,-8046.4476 3983,-8170.5998 4064.5752,-8247.6297 4089.6527,-8620.6309 4094.8692,-8718.2032"/> +<polygon fill="#000000" stroke="#000000" points="4093.1342,-8718.5366 4095.144,-8723.4379 4096.6294,-8718.353 4093.1342,-8718.5366"/> +</g> +<!-- github.com/containers/image/v5/transports->sort --> +<g id="edge163" class="edge"> +<title>github.com/containers/image/v5/transports->sort</title> +<path fill="none" stroke="#000000" d="M2701.5524,-2531.2936C2924.0596,-2528.932 3378.7986,-2532.4718 3522,-2596.5998 3783.8822,-2713.8751 3866.2828,-2786.4684 3983,-3048.5998 4044.6125,-3186.9733 3962.7075,-4280.9326 4041,-4410.5998 4046.3978,-4419.5396 4055.3087,-4426.2405 4064.4022,-4431.137"/> +<polygon fill="#000000" stroke="#000000" points="4063.6151,-4432.7 4068.8664,-4433.3977 4065.1964,-4429.5775 4063.6151,-4432.7"/> +</g> +<!-- github.com/containers/image/v5/transports->sync --> +<g id="edge164" class="edge"> +<title>github.com/containers/image/v5/transports->sync</title> +<path fill="none" stroke="#000000" d="M2701.6876,-2517.245C3028.1735,-2475.9302 3887.0058,-2376.3217 3983,-2458.5998 3996.8972,-2470.5113 4075.914,-3042.0235 4092.8176,-3165.3169"/> +<polygon fill="#000000" stroke="#000000" points="4091.1172,-3165.799 4093.5299,-3170.5151 4094.5848,-3165.3238 4091.1172,-3165.799"/> +</g> +<!-- github.com/containers/image/v5/transports->github.com/containers/image/v5/types --> +<g id="edge162" class="edge"> +<title>github.com/containers/image/v5/transports->github.com/containers/image/v5/types</title> +<path fill="none" stroke="#000000" d="M2577.6904,-2551.8088C2589.5886,-2734.6481 2687.5006,-4258.6014 2727,-5494.5998 2758.4045,-6477.2937 2693.6172,-8939.6602 2785,-9918.5998 2821.1803,-10306.1824 2926.1446,-10770.5776 2951.4587,-10879.1221"/> +<polygon fill="#000000" stroke="#000000" points="2949.8299,-10879.8425 2952.672,-10884.313 2953.238,-10879.0459 2949.8299,-10879.8425"/> +</g> +<!-- github.com/containers/image/v5/types->context --> +<g id="edge165" class="edge"> +<title>github.com/containers/image/v5/types->context</title> +<path fill="none" stroke="#000000" d="M2960.0077,-10884.216C2979.7809,-10762.3579 3091.5073,-10058.1993 3129,-9479.5998 3174.6471,-8775.1589 3076.853,-3823.8719 3187,-3126.5998 3264.5559,-2635.6419 3431.4081,-2548.3203 3522,-2059.5998 3540.85,-1957.9089 3518.5487,-1213.7869 3580,-1130.5998 3693.7412,-976.6275 3804.251,-1047.1078 3983,-978.5998 4009.9789,-968.2598 4040.5067,-956.3609 4063.0249,-947.5462"/> +<polygon fill="#000000" stroke="#000000" points="4063.6868,-949.1665 4067.7044,-945.7137 4062.4105,-945.9075 4063.6868,-949.1665"/> +</g> +<!-- github.com/containers/image/v5/types->io --> +<g id="edge170" class="edge"> +<title>github.com/containers/image/v5/types->io</title> +<path fill="none" stroke="#000000" d="M2963.4613,-10920.7432C2990.2226,-10996.91 3092.6661,-11298.8072 3129,-11556.5998 3148.668,-11696.1463 3094.7115,-13986.0968 3187,-14092.5998 3285.8563,-14206.6822 3388.9625,-14077.2669 3522,-14148.5998 3555.2546,-14166.4305 3545.3844,-14195.5811 3580,-14210.5998 3662.1561,-14246.2449 3913.8538,-14267.512 3983,-14210.5998 4086.5062,-14125.4069 3963.9904,-14024.3308 4041,-13914.5998 4046.7751,-13906.3708 4055.3173,-13899.8417 4063.9565,-13894.837"/> +<polygon fill="#000000" stroke="#000000" points="4065.2044,-13896.1489 4068.7492,-13892.2123 4063.5231,-13893.0791 4065.2044,-13896.1489"/> +</g> +<!-- github.com/containers/image/v5/types->time --> +<g id="edge171" class="edge"> +<title>github.com/containers/image/v5/types->time</title> +<path fill="none" stroke="#000000" d="M2978.5208,-10920.6339C3017.4046,-10952.1666 3103.1406,-11016.9695 3187,-11048.5998 3522.0708,-11174.9828 3710.8194,-10940.8707 3983,-11173.5998 4046.8818,-11228.2222 3988.2945,-11288.1275 4041,-11353.5998 4047.1372,-11361.2236 4055.6136,-11367.4031 4064.0752,-11372.225"/> +<polygon fill="#000000" stroke="#000000" points="4063.5299,-11373.92 4068.7596,-11374.7646 4065.1981,-11370.8431 4063.5299,-11373.92"/> +</g> +<!-- github.com/containers/image/v5/types->github.com/containers/image/v5/docker/reference --> +<g id="edge166" class="edge"> +<title>github.com/containers/image/v5/types->github.com/containers/image/v5/docker/reference</title> +<path fill="none" stroke="#000000" d="M2959.9155,-10884.2097C2979.0966,-10762.3113 3087.6992,-10057.9399 3129,-9479.5998 3149.591,-9191.2623 3090.6447,-7151.1401 3187,-6878.5998 3216.3105,-6795.6952 3290.5656,-6719.4233 3329.6195,-6683.3966"/> +<polygon fill="#000000" stroke="#000000" points="3331.2381,-6684.2874 3333.7444,-6679.6204 3328.8748,-6681.7058 3331.2381,-6684.2874"/> +</g> +<!-- github.com/containers/image/v5/types->github.com/opencontainers/go-digest --> +<g id="edge168" class="edge"> +<title>github.com/containers/image/v5/types->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2984.9541,-10920.6944C3026.8583,-10946.6625 3109.3512,-10993.3754 3187,-11011.5998 3350.5555,-11049.9869 3546.4209,-11044.0127 3667.4169,-11034.5661"/> +<polygon fill="#000000" stroke="#000000" points="3667.5587,-11036.3105 3672.405,-11034.1714 3667.2825,-11032.8214 3667.5587,-11036.3105"/> +</g> +<!-- github.com/containers/image/v5/types->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge169" class="edge"> +<title>github.com/containers/image/v5/types->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M2978.4669,-10920.7377C3011.8495,-10949.2868 3077.6979,-11006.9893 3129,-11060.5998 3156.6979,-11089.544 3152.9116,-11108.5541 3187,-11129.5998 3200.7176,-11138.0689 3216.0877,-11144.7449 3231.8057,-11150.0074"/> +<polygon fill="#000000" stroke="#000000" points="3231.3973,-11151.7147 3236.6934,-11151.5956 3232.479,-11148.386 3231.3973,-11151.7147"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression/types --> +<g id="node79" class="node"> +<title>github.com/containers/image/v5/pkg/compression/types</title> +<g id="a_node79"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression/types" xlink:title="github.com/containers/image/v5/pkg/compression/types" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3503.5,-14445.5998C3503.5,-14445.5998 3205.5,-14445.5998 3205.5,-14445.5998 3199.5,-14445.5998 3193.5,-14439.5998 3193.5,-14433.5998 3193.5,-14433.5998 3193.5,-14421.5998 3193.5,-14421.5998 3193.5,-14415.5998 3199.5,-14409.5998 3205.5,-14409.5998 3205.5,-14409.5998 3503.5,-14409.5998 3503.5,-14409.5998 3509.5,-14409.5998 3515.5,-14415.5998 3515.5,-14421.5998 3515.5,-14421.5998 3515.5,-14433.5998 3515.5,-14433.5998 3515.5,-14439.5998 3509.5,-14445.5998 3503.5,-14445.5998"/> +<text text-anchor="middle" x="3354.5" y="-14423.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression/types</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/types->github.com/containers/image/v5/pkg/compression/types --> +<g id="edge167" class="edge"> +<title>github.com/containers/image/v5/types->github.com/containers/image/v5/pkg/compression/types</title> +<path fill="none" stroke="#000000" d="M2963.4649,-10920.7427C2990.2405,-10996.9074 3092.7331,-11298.7978 3129,-11556.5998 3169.1765,-11842.1931 3106.2154,-13874.7398 3187,-14151.5998 3217.5566,-14256.3214 3298.465,-14361.338 3335.5031,-14405.6727"/> +<polygon fill="#000000" stroke="#000000" points="3334.1783,-14406.8164 3338.7342,-14409.5195 3336.8583,-14404.5653 3334.1783,-14406.8164"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->encoding/json --> +<g id="edge227" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->encoding/json</title> +<path fill="none" stroke="#000000" d="M3824.8594,-4519.6046C3873.5276,-4542.4657 3949.9912,-4586.9942 3983,-4652.5998 4061.3623,-4808.3463 3953.7291,-17054.6647 4041,-17205.5998 4043.2654,-17209.5179 4046.2144,-17213.0015 4049.5701,-17216.0893"/> +<polygon fill="#000000" stroke="#000000" points="4048.7236,-17217.6633 4053.6796,-17219.5346 4050.9723,-17214.9812 4048.7236,-17217.6633"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->fmt --> +<g id="edge228" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->fmt</title> +<path fill="none" stroke="#000000" d="M3824.7124,-4519.6791C3873.2425,-4542.6103 3949.5764,-4587.2045 3983,-4652.5998 4025.4195,-4735.5961 4036.7103,-7914.4902 4041,-8007.5998 4053.9165,-8287.9564 4085.1511,-8627.5027 4093.7666,-8718.3346"/> +<polygon fill="#000000" stroke="#000000" points="4092.0388,-8718.6523 4094.2542,-8723.4643 4095.5231,-8718.3211 4092.0388,-8718.6523"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sort --> +<g id="edge230" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sort</title> +<path fill="none" stroke="#000000" d="M3877.5693,-4483.5773C3941.1016,-4471.6587 4020.4192,-4456.7787 4063.8574,-4448.6298"/> +<polygon fill="#000000" stroke="#000000" points="4064.3818,-4450.312 4068.9733,-4447.67 4063.7364,-4446.872 4064.3818,-4450.312"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->strings --> +<g id="edge231" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->strings</title> +<path fill="none" stroke="#000000" d="M3824.6021,-4519.7358C3873.0287,-4542.7202 3949.2654,-4587.3644 3983,-4652.5998 4038.217,-4759.3776 4026.9838,-6696.2099 4041,-6815.5998 4051.4883,-6904.9394 4077.554,-7008.9442 4089.6487,-7054.3764"/> +<polygon fill="#000000" stroke="#000000" points="4088.0002,-7054.9863 4090.9831,-7059.3641 4091.3813,-7054.0816 4088.0002,-7054.9863"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sync --> +<g id="edge232" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sync</title> +<path fill="none" stroke="#000000" d="M3869.0419,-4483.594C3910.3127,-4470.4434 3956.2589,-4448.1042 3983,-4410.5998 3987.5408,-4404.2314 4078.9266,-3380.2631 4093.9255,-3211.9012"/> +<polygon fill="#000000" stroke="#000000" points="4095.6862,-3211.8572 4094.3868,-3206.7216 4092.2,-3211.5466 4095.6862,-3211.8572"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->net/http --> +<g id="edge229" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->net/http</title> +<path fill="none" stroke="#000000" d="M3873.1245,-4483.5233C3913.9314,-4470.4545 3958.2767,-4448.207 3983,-4410.5998 4036.5369,-4329.1638 4031.9637,-999.6378 4041,-902.5998 4051.5234,-789.5919 4079.2055,-656.4882 4090.7877,-603.7704"/> +<polygon fill="#000000" stroke="#000000" points="4092.554,-603.8871 4091.9227,-598.6274 4089.1362,-603.1327 4092.554,-603.8871"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->fmt --> +<g id="edge233" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->fmt</title> +<path fill="none" stroke="#000000" d="M2591.4474,-2256.8922C2623.7418,-2297.7161 2699.3417,-2400.6314 2727,-2501.5998 2755.4117,-2605.3186 2720.5959,-6286.4782 2785,-6372.5998 2880.8901,-6500.8247 3031.2312,-6352.8016 3129,-6479.5998 3217.5595,-6594.4543 3095.5819,-7671.0074 3187,-7783.5998 3283.8922,-7902.9344 3407.7689,-7766.7403 3522,-7869.5998 3576.9694,-7919.097 3524.0265,-7977.241 3580,-8025.5998 3718.5855,-8145.332 3855.6367,-7979.9932 3983,-8111.5998 4069.4514,-8200.9315 4091.104,-8615.3626 4095.1877,-8718.3729"/> +<polygon fill="#000000" stroke="#000000" points="4093.4479,-8718.6711 4095.3903,-8723.5995 4096.9452,-8718.5354 4093.4479,-8718.6711"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->strings --> +<g id="edge241" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->strings</title> +<path fill="none" stroke="#000000" d="M2590.5798,-2256.7712C2621.4737,-2297.7594 2694.9952,-2401.6476 2727,-2501.5998 2805.7316,-2747.4813 2750.1754,-2823.7803 2785,-3079.5998 2906.4815,-3971.9967 2982.0706,-4188.0383 3129,-5076.5998 3155.8688,-5239.0898 3088.3908,-5310.6864 3187,-5442.5998 3302.8141,-5597.5289 3403.3436,-5550.8063 3580,-5629.5998 3757.2221,-5708.6456 3870.8233,-5639.2575 3983,-5797.5998 4048.4931,-5890.0461 4026.0979,-6703.2896 4041,-6815.5998 4052.8319,-6904.7714 4078.2096,-7008.8622 4089.8886,-7054.3464"/> +<polygon fill="#000000" stroke="#000000" points="4088.2332,-7054.9353 4091.1763,-7059.3399 4091.6223,-7054.0613 4088.2332,-7054.9353"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->unicode --> +<g id="edge242" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->unicode</title> +<path fill="none" stroke="#000000" d="M2710.6805,-2225.2988C3028.2118,-2197.094 3811.908,-2147.7011 3983,-2327.5998 4042.3466,-2390.0011 4030.4722,-3791.1299 4041,-3876.5998 4051.9567,-3965.5519 4077.7825,-4069.1836 4089.7323,-4114.4568"/> +<polygon fill="#000000" stroke="#000000" points="4088.0771,-4115.0427 4091.0505,-4119.427 4091.4602,-4114.1454 4088.0771,-4115.0427"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/registry/api/errcode --> +<g id="edge235" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M2607.2313,-2256.7573C2641.2329,-2278.3576 2695.387,-2317.4855 2727,-2364.5998 3102.5269,-2924.2662 2908.9074,-3195.6687 3187,-3809.5998 3195.7254,-3828.8625 3562.6933,-4449.4482 3580,-4461.5998 3594.7409,-4471.9499 3611.5076,-4479.7731 3628.9271,-4485.6635"/> +<polygon fill="#000000" stroke="#000000" points="3628.5602,-4487.3846 3633.8564,-4487.2742 3629.6473,-4484.0577 3628.5602,-4487.3846"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/opencontainers/go-digest --> +<g id="edge237" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2591.4975,-2256.8785C2623.8918,-2297.6752 2699.6786,-2400.5397 2727,-2501.5998 2828.1762,-2875.8447 2676.5148,-9107.4079 2785,-9479.5998 2865.6809,-9756.4006 3014.3611,-9765.0511 3129,-10029.5998 3169.035,-10121.9874 3123.3111,-10170.6127 3187,-10248.5998 3292.105,-10377.301 3422.7866,-10281.304 3522,-10414.5998 3613.528,-10537.5701 3519.3418,-10613.8175 3580,-10754.5998 3624.4954,-10857.8697 3716.5702,-10957.8836 3759.0859,-11000.7559"/> +<polygon fill="#000000" stroke="#000000" points="3758.0282,-11002.1737 3762.7973,-11004.48 3760.5074,-10999.7031 3758.0282,-11002.1737"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->net/http --> +<g id="edge238" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->net/http</title> +<path fill="none" stroke="#000000" d="M2579.0422,-2220.511C2591.2344,-2137.7827 2648.8313,-1788.8099 2785,-1539.5998 2897.4304,-1333.8345 3025.4795,-1349.9886 3129,-1139.5998 3185.1078,-1025.5699 3122.1995,-968.9241 3187,-859.5998 3286.674,-691.4408 3387.037,-716.0117 3522,-574.5998 3551.0928,-544.1168 3542.1726,-518.1657 3580,-499.5998 3740.7889,-420.6839 3812.4791,-444.7966 3983,-499.5998 4019.1279,-511.2109 4053.4993,-539.1094 4074.6462,-558.9114"/> +<polygon fill="#000000" stroke="#000000" points="4073.6064,-560.3373 4078.4383,-562.5089 4076.0153,-557.7981 4073.6064,-560.3373"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->net/url --> +<g id="edge239" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->net/url</title> +<path fill="none" stroke="#000000" d="M2582.9866,-2220.582C2604.7289,-2161.8497 2679.6343,-1972.3744 2785,-1844.5998 2930.9928,-1667.5577 2976.314,-1611.535 3187,-1520.5998 3512.5607,-1380.083 3638.3298,-1383.31 3983,-1466.5998 4012.7402,-1473.7866 4043.4369,-1490.2208 4065.3169,-1503.7673"/> +<polygon fill="#000000" stroke="#000000" points="4064.4064,-1505.2619 4069.5724,-1506.4345 4066.2651,-1502.2962 4064.4064,-1505.2619"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->regexp --> +<g id="edge240" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->regexp</title> +<path fill="none" stroke="#000000" d="M2617.3121,-2220.5613C2718.6201,-2173.7844 2983.8677,-2038.7635 3129,-1847.5998 3175.2433,-1786.6896 3124.8085,-1730.1049 3187,-1685.5998 3474.7082,-1479.7119 3711.2659,-1453.0465 3983,-1679.5998 4076.7206,-1757.7376 4092.9357,-2171.3752 4095.5339,-2274.3632"/> +<polygon fill="#000000" stroke="#000000" points="4093.7898,-2274.6333 4095.6606,-2279.5894 4097.2888,-2274.5484 4093.7898,-2274.6333"/> +</g> +<!-- github.com/docker/distribution/reference --> +<g id="node90" class="node"> +<title>github.com/docker/distribution/reference</title> +<g id="a_node90"><a xlink:href="https://godoc.org/github.com/docker/distribution/reference" xlink:title="github.com/docker/distribution/reference" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3064.5,-5126.5998C3064.5,-5126.5998 2849.5,-5126.5998 2849.5,-5126.5998 2843.5,-5126.5998 2837.5,-5120.5998 2837.5,-5114.5998 2837.5,-5114.5998 2837.5,-5102.5998 2837.5,-5102.5998 2837.5,-5096.5998 2843.5,-5090.5998 2849.5,-5090.5998 2849.5,-5090.5998 3064.5,-5090.5998 3064.5,-5090.5998 3070.5,-5090.5998 3076.5,-5096.5998 3076.5,-5102.5998 3076.5,-5102.5998 3076.5,-5114.5998 3076.5,-5114.5998 3076.5,-5120.5998 3070.5,-5126.5998 3064.5,-5126.5998"/> +<text text-anchor="middle" x="2957" y="-5104.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/reference</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/reference --> +<g id="edge234" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M2591.1269,-2256.6072C2623.1263,-2297.264 2698.7391,-2400.5252 2727,-2501.5998 2862.9988,-2987.998 2660.0824,-4284.2385 2785,-4773.5998 2817.4784,-4900.8332 2904.8712,-5034.6327 2941.0156,-5086.3736"/> +<polygon fill="#000000" stroke="#000000" points="2939.6353,-5087.4533 2943.9401,-5090.5408 2942.5003,-5085.4428 2939.6353,-5087.4533"/> +</g> +<!-- github.com/gorilla/mux --> +<g id="node94" class="node"> +<title>github.com/gorilla/mux</title> +<g id="a_node94"><a xlink:href="https://godoc.org/github.com/gorilla/mux" xlink:title="github.com/gorilla/mux" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3842.5,-2377.5998C3842.5,-2377.5998 3720.5,-2377.5998 3720.5,-2377.5998 3714.5,-2377.5998 3708.5,-2371.5998 3708.5,-2365.5998 3708.5,-2365.5998 3708.5,-2353.5998 3708.5,-2353.5998 3708.5,-2347.5998 3714.5,-2341.5998 3720.5,-2341.5998 3720.5,-2341.5998 3842.5,-2341.5998 3842.5,-2341.5998 3848.5,-2341.5998 3854.5,-2347.5998 3854.5,-2353.5998 3854.5,-2353.5998 3854.5,-2365.5998 3854.5,-2365.5998 3854.5,-2371.5998 3848.5,-2377.5998 3842.5,-2377.5998"/> +<text text-anchor="middle" x="3781.5" y="-2355.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/gorilla/mux</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/gorilla/mux --> +<g id="edge236" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/gorilla/mux</title> +<path fill="none" stroke="#000000" d="M2710.6402,-2252.0695C2963.1111,-2277.4213 3501.6091,-2331.4946 3703.1787,-2351.7352"/> +<polygon fill="#000000" stroke="#000000" points="3703.0192,-2353.4779 3708.1691,-2352.2363 3703.369,-2349.9954 3703.0192,-2353.4779"/> +</g> +<!-- github.com/docker/distribution/registry/client->bytes --> +<g id="edge243" class="edge"> +<title>github.com/docker/distribution/registry/client->bytes</title> +<path fill="none" stroke="#000000" d="M406.3258,-5303.9314C410.5576,-5536.0898 456.8927,-7906.7662 596,-8592.5998 1076.9464,-10963.7859 1537.6204,-11469.13 2426,-13719.5998 2444.4095,-13766.2353 2745.4969,-14516.7244 2785,-14547.5998 2890.0613,-14629.7152 3885.2297,-14709.2738 3983,-14618.5998 4045.8271,-14560.3329 4022.3915,-13933.242 4041,-13849.5998 4050.949,-13804.8808 4071.9708,-13755.5492 4084.9504,-13727.4933"/> +<polygon fill="#000000" stroke="#000000" points="4086.5919,-13728.1138 4087.1186,-13722.8427 4083.4197,-13726.6349 4086.5919,-13728.1138"/> +</g> +<!-- github.com/docker/distribution/registry/client->context --> +<g id="edge244" class="edge"> +<title>github.com/docker/distribution/registry/client->context</title> +<path fill="none" stroke="#000000" d="M406.4957,-5267.4235C412.6123,-5046.9295 475.3566,-2890.1154 596,-2637.5998 776.5988,-2259.5932 2548.5253,-855.9745 2785,-747.5998 3269.6817,-525.4737 3537.0278,-393.4265 3983,-685.5998 4062.2773,-737.5374 4086.6287,-859.9035 4093.5083,-911.2623"/> +<polygon fill="#000000" stroke="#000000" points="4091.7822,-911.5614 4094.1568,-916.2969 4095.2536,-911.1142 4091.7822,-911.5614"/> +</g> +<!-- github.com/docker/distribution/registry/client->encoding/json --> +<g id="edge245" class="edge"> +<title>github.com/docker/distribution/registry/client->encoding/json</title> +<path fill="none" stroke="#000000" d="M406.2673,-5303.846C411.0805,-5629.5194 480.6531,-10221.7673 596,-11562.5998 708.4685,-12869.9729 787.2363,-13192.7909 1012,-14485.5998 1150.2678,-15280.8961 1204.5263,-15476.2337 1378,-16264.5998 1526.1327,-16937.8016 1212.3501,-17321.7115 1732,-17774.5998 2110.0202,-18104.0542 3601.0001,-18256.4315 3983,-17931.5998 4035.6945,-17886.7913 4083.6583,-17377.3248 4093.9629,-17261.104"/> +<polygon fill="#000000" stroke="#000000" points="4095.7283,-17261.0049 4094.425,-17255.8703 4092.2419,-17260.697 4095.7283,-17261.0049"/> +</g> +<!-- github.com/docker/distribution/registry/client->errors --> +<g id="edge246" class="edge"> +<title>github.com/docker/distribution/registry/client->errors</title> +<path fill="none" stroke="#000000" d="M417.889,-5267.4543C444.4829,-5228.5803 513.0042,-5137.5561 596,-5097.5998 763.7405,-5016.8453 827.0403,-5069.7682 1012,-5048.5998 1566.2025,-4985.1722 3057.2669,-4709.0765 3522,-5017.5998 3570.3534,-5049.7003 3535.0554,-5097.8782 3580,-5134.5998 3723.5073,-5251.8515 3864.7196,-5098.9392 3983,-5241.5998 4084.2492,-5363.7187 4094.8665,-6575.8128 4095.8956,-6759.3623"/> +<polygon fill="#000000" stroke="#000000" points="4094.1462,-6759.5002 4095.9233,-6764.4906 4097.6462,-6759.4812 4094.1462,-6759.5002"/> +</g> +<!-- github.com/docker/distribution/registry/client->fmt --> +<g id="edge247" class="edge"> +<title>github.com/docker/distribution/registry/client->fmt</title> +<path fill="none" stroke="#000000" d="M406.3746,-5303.7462C412.0702,-5556.9815 490.6344,-8375.5998 1166,-8375.5998 1166,-8375.5998 1166,-8375.5998 2195.5,-8375.5998 2363.627,-8375.5998 3479.2745,-8472.8452 3522,-8477.5998 3727.6461,-8500.4847 3811.9008,-8426.2422 3983,-8542.5998 4045.9813,-8585.4309 4078.0035,-8675.7095 4090.1154,-8718.354"/> +<polygon fill="#000000" stroke="#000000" points="4088.4685,-8718.9639 4091.4945,-8723.312 4091.8405,-8718.0259 4088.4685,-8718.9639"/> +</g> +<!-- github.com/docker/distribution/registry/client->io --> +<g id="edge257" class="edge"> +<title>github.com/docker/distribution/registry/client->io</title> +<path fill="none" stroke="#000000" d="M406.3638,-5303.8437C411.5834,-5561.9929 473.05,-8485.8255 596,-9338.5998 786.735,-10661.5275 1054.2037,-14260.7572 2023,-15181.5998 2181.023,-15331.801 3831.0749,-15429.9661 3983,-15273.5998 4088.3199,-15165.2012 3963.7471,-14044.5018 4041,-13914.5998 4046.3806,-13905.5523 4055.287,-13898.6869 4064.3816,-13893.6257"/> +<polygon fill="#000000" stroke="#000000" points="4065.2313,-13895.1563 4068.8468,-13891.2844 4063.6059,-13892.0565 4065.2313,-13895.1563"/> +</g> +<!-- github.com/docker/distribution/registry/client->io/ioutil --> +<g id="edge258" class="edge"> +<title>github.com/docker/distribution/registry/client->io/ioutil</title> +<path fill="none" stroke="#000000" d="M406.2001,-5303.9971C409.5532,-5606.2368 455.7209,-9546.0087 596,-10693.5998 689.4221,-11457.864 842.6581,-11626.74 954,-12388.5998 999.4638,-12699.6871 979.4871,-12780.8936 1012,-13093.5998 1146.8041,-14390.1334 914.3398,-14785.3261 1378,-16003.5998 1476.7943,-16263.1833 1489.1398,-16383.8303 1732,-16518.5998 2003.4504,-16669.2348 2819.501,-16545.8185 3129,-16521.5998 3176.7879,-16517.8604 3939.4044,-16418.527 3983,-16398.5998 4021.6242,-16380.945 4057.0152,-16345.8534 4077.5414,-16322.8079"/> +<polygon fill="#000000" stroke="#000000" points="4078.961,-16323.8439 4080.9523,-16318.9349 4076.3344,-16321.5306 4078.961,-16323.8439"/> +</g> +<!-- github.com/docker/distribution/registry/client->strconv --> +<g id="edge261" class="edge"> +<title>github.com/docker/distribution/registry/client->strconv</title> +<path fill="none" stroke="#000000" d="M409.8056,-5267.5307C428.0354,-5182.7838 509.1356,-4826.0269 596,-4759.5998 745.5451,-4645.2394 3840.6484,-4529.4005 3983,-4652.5998 4050.9155,-4711.3778 4026.4284,-4963.9712 4041,-5052.5998 4057.9367,-5155.6132 4081.5344,-5277.934 4091.3896,-5328.2329"/> +<polygon fill="#000000" stroke="#000000" points="4089.7313,-5328.8703 4092.4112,-5333.4399 4093.1658,-5328.1965 4089.7313,-5328.8703"/> +</g> +<!-- github.com/docker/distribution/registry/client->strings --> +<g id="edge262" class="edge"> +<title>github.com/docker/distribution/registry/client->strings</title> +<path fill="none" stroke="#000000" d="M418.4444,-5303.6207C530.509,-5463.9721 1385.7377,-6647.8925 2426,-6971.5998 2523.953,-7002.0807 3212.4127,-6989.1993 3522,-6992.5998 3726.9001,-6994.8505 3787.8345,-6937.1533 3983,-6999.5998 4018.2666,-7010.884 4052.3387,-7037.1913 4073.6819,-7056.1363"/> +<polygon fill="#000000" stroke="#000000" points="4072.6279,-7057.5419 4077.5164,-7059.5827 4074.9676,-7054.9388 4072.6279,-7057.5419"/> +</g> +<!-- github.com/docker/distribution/registry/client->time --> +<g id="edge263" class="edge"> +<title>github.com/docker/distribution/registry/client->time</title> +<path fill="none" stroke="#000000" d="M406.06,-5303.6177C406.9982,-5546.8314 421.1666,-8175.1095 596,-8463.5998 692.1929,-8622.3264 837.158,-8534.3945 954,-8678.5998 1621.9613,-9502.9916 1164.1197,-10039.3264 1732,-10935.5998 1847.8733,-11118.4802 2527.6205,-11709.5998 2576.5,-11709.5998 2576.5,-11709.5998 2576.5,-11709.5998 2957,-11709.5998 3015.1595,-11709.5998 3932.1592,-11529.8441 3983,-11501.5998 4024.9919,-11478.2715 4061.2671,-11434.4107 4080.7512,-11407.8165"/> +<polygon fill="#000000" stroke="#000000" points="4082.2156,-11408.778 4083.7324,-11403.7023 4079.3815,-11406.7243 4082.2156,-11408.778"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/errcode --> +<g id="edge250" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M409.0303,-5267.3029C424.7874,-5174.4119 500.3139,-4756.2065 596,-4673.5998 1470.9162,-3918.2766 2031.2997,-4468.2609 3187,-4449.5998 3335.8695,-4447.196 3373.8851,-4434.4381 3522,-4449.5998 3548.1869,-4452.2804 3554.201,-4456.3703 3580,-4461.5998 3614.0918,-4468.5102 3651.4543,-4475.9611 3684.647,-4482.5376"/> +<polygon fill="#000000" stroke="#000000" points="3684.6925,-4484.3305 3689.9372,-4483.5853 3685.3725,-4480.8972 3684.6925,-4484.3305"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/v2 --> +<g id="edge251" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/v2</title> +<path fill="none" stroke="#000000" d="M408.3746,-5267.511C419.4906,-5187.0055 471.1927,-4854.6971 596,-4614.5998 1190.5194,-3470.8963 1521.0377,-3282.5075 2426,-2364.5998 2465.4957,-2324.5391 2516.6393,-2283.7479 2547.9831,-2259.8281"/> +<polygon fill="#000000" stroke="#000000" points="2549.1623,-2261.1299 2552.0823,-2256.7099 2547.0433,-2258.3443 2549.1623,-2261.1299"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/opencontainers/go-digest --> +<g id="edge256" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M406.496,-5303.863C413.3372,-5553.0549 490.2894,-8270.5708 596,-8397.5998 700.8749,-8523.6246 801.7553,-8425.7514 954,-8486.5998 1127.7375,-8556.0384 1176.1471,-8574.9638 1320,-8694.5998 1875.8325,-9156.8609 1919.9893,-9371.2186 2368,-9938.5998 2561.1891,-10183.2633 2538.5505,-10311.6945 2785,-10502.5998 2915.3621,-10603.5812 3006.856,-10530.8191 3129,-10641.5998 3170.2302,-10678.9943 3143.9142,-10717.3594 3187,-10752.5998 3306.9828,-10850.7354 3404.9625,-10747.9696 3522,-10849.5998 3569.4643,-10890.8157 3531.1847,-10938.9933 3580,-10978.5998 3604.6782,-10998.6226 3636.4639,-11010.0506 3667.4628,-11016.4365"/> +<polygon fill="#000000" stroke="#000000" points="3667.1854,-11018.1655 3672.43,-11017.4187 3667.8644,-11014.732 3667.1854,-11018.1655"/> +</g> +<!-- github.com/docker/distribution/registry/client->net/http --> +<g id="edge259" class="edge"> +<title>github.com/docker/distribution/registry/client->net/http</title> +<path fill="none" stroke="#000000" d="M406.2463,-5267.5509C409.6086,-5029.2561 448.5944,-2506.7773 596,-2216.5998 1256.3193,-916.7183 1798.1358,-648.1871 3187,-204.5998 3524.3901,-96.8413 3704.3332,-23.9934 3983,-242.5998 4034.4781,-282.983 4078.0228,-487.3263 4091.6085,-557.1624"/> +<polygon fill="#000000" stroke="#000000" points="4089.9423,-557.7641 4092.6089,-562.3414 4093.3788,-557.1002 4089.9423,-557.7641"/> +</g> +<!-- github.com/docker/distribution/registry/client->net/url --> +<g id="edge260" class="edge"> +<title>github.com/docker/distribution/registry/client->net/url</title> +<path fill="none" stroke="#000000" d="M406.3059,-5267.4982C409.5773,-5085.2455 441.1032,-3579.508 596,-3162.5998 706.4826,-2865.2333 797.2803,-2816.1137 1012,-2582.5998 1776.4364,-1751.253 2073.7667,-1458.8947 3187,-1268.5998 3540.4819,-1208.176 3678.2795,-1211.534 3983,-1400.5998 4025.7559,-1427.128 4062.2659,-1474.3218 4081.4888,-1502.2084"/> +<polygon fill="#000000" stroke="#000000" points="4080.1609,-1503.3673 4084.4238,-1506.5122 4083.0525,-1501.3953 4080.1609,-1503.3673"/> +</g> +<!-- github.com/docker/distribution --> +<g id="node89" class="node"> +<title>github.com/docker/distribution</title> +<g id="a_node89"><a xlink:href="https://godoc.org/github.com/docker/distribution" xlink:title="github.com/docker/distribution" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2657,-5792.5998C2657,-5792.5998 2496,-5792.5998 2496,-5792.5998 2490,-5792.5998 2484,-5786.5998 2484,-5780.5998 2484,-5780.5998 2484,-5768.5998 2484,-5768.5998 2484,-5762.5998 2490,-5756.5998 2496,-5756.5998 2496,-5756.5998 2657,-5756.5998 2657,-5756.5998 2663,-5756.5998 2669,-5762.5998 2669,-5768.5998 2669,-5768.5998 2669,-5780.5998 2669,-5780.5998 2669,-5786.5998 2663,-5792.5998 2657,-5792.5998"/> +<text text-anchor="middle" x="2576.5" y="-5770.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution --> +<g id="edge248" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M538.2457,-5302.1131C689.6757,-5319.5673 945.3206,-5344.5998 1166,-5344.5998 1166,-5344.5998 1166,-5344.5998 1848.5,-5344.5998 2010.9984,-5344.5998 2436.9476,-5666.2293 2549.1089,-5753.162"/> +<polygon fill="#000000" stroke="#000000" points="2548.2557,-5754.715 2553.2787,-5756.3979 2550.4015,-5751.9499 2548.2557,-5754.715"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/reference --> +<g id="edge249" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M435.6951,-5267.5275C472.0773,-5246.3121 536.2616,-5211.8399 596,-5194.5998 1134.4118,-5039.2176 1288.1154,-5049.5998 1848.5,-5049.5998 1848.5,-5049.5998 1848.5,-5049.5998 2195.5,-5049.5998 2420.2459,-5049.5998 2681.1908,-5075.4618 2831.8874,-5092.9408"/> +<polygon fill="#000000" stroke="#000000" points="2832.0326,-5094.7194 2837.2014,-5093.559 2832.4371,-5091.2428 2832.0326,-5094.7194"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge --> +<g id="node95" class="node"> +<title>github.com/docker/distribution/registry/client/auth/challenge</title> +<g id="a_node95"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" xlink:title="github.com/docker/distribution/registry/client/auth/challenge" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3943,-1899.5998C3943,-1899.5998 3620,-1899.5998 3620,-1899.5998 3614,-1899.5998 3608,-1893.5998 3608,-1887.5998 3608,-1887.5998 3608,-1875.5998 3608,-1875.5998 3608,-1869.5998 3614,-1863.5998 3620,-1863.5998 3620,-1863.5998 3943,-1863.5998 3943,-1863.5998 3949,-1863.5998 3955,-1869.5998 3955,-1875.5998 3955,-1875.5998 3955,-1887.5998 3955,-1887.5998 3955,-1893.5998 3949,-1899.5998 3943,-1899.5998"/> +<text text-anchor="middle" x="3781.5" y="-1877.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth/challenge</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/auth/challenge --> +<g id="edge252" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/auth/challenge</title> +<path fill="none" stroke="#000000" d="M407.2599,-5267.5098C417.1375,-5128.5845 484.7017,-4229.8812 596,-3984.5998 905.2705,-3303.0237 1117.7152,-2255.9163 2785,-1508.5998 3165.2463,-1338.1645 3649.0955,-1757.6971 3758.7326,-1859.8247"/> +<polygon fill="#000000" stroke="#000000" points="3757.6264,-1861.1862 3762.4741,-1863.3223 3760.0166,-1858.6294 3757.6264,-1861.1862"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport --> +<g id="node96" class="node"> +<title>github.com/docker/distribution/registry/client/transport</title> +<g id="a_node96"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/transport" xlink:title="github.com/docker/distribution/registry/client/transport" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3927.5,-4702.5998C3927.5,-4702.5998 3635.5,-4702.5998 3635.5,-4702.5998 3629.5,-4702.5998 3623.5,-4696.5998 3623.5,-4690.5998 3623.5,-4690.5998 3623.5,-4678.5998 3623.5,-4678.5998 3623.5,-4672.5998 3629.5,-4666.5998 3635.5,-4666.5998 3635.5,-4666.5998 3927.5,-4666.5998 3927.5,-4666.5998 3933.5,-4666.5998 3939.5,-4672.5998 3939.5,-4678.5998 3939.5,-4678.5998 3939.5,-4690.5998 3939.5,-4690.5998 3939.5,-4696.5998 3933.5,-4702.5998 3927.5,-4702.5998"/> +<text text-anchor="middle" x="3781.5" y="-4680.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/transport</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/transport --> +<g id="edge253" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/transport</title> +<path fill="none" stroke="#000000" d="M407.4603,-5267.4037C414.0589,-5198.6392 447.6098,-4953.3275 596,-4850.5998 845.4475,-4677.9121 3001.9977,-4679.2319 3617.8891,-4683.1926"/> +<polygon fill="#000000" stroke="#000000" points="3618.2669,-4684.945 3623.2781,-4683.2276 3618.2896,-4681.445 3618.2669,-4684.945"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache --> +<g id="node97" class="node"> +<title>github.com/docker/distribution/registry/storage/cache</title> +<g id="a_node97"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache" xlink:title="github.com/docker/distribution/registry/storage/cache" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1308,-5424.5998C1308,-5424.5998 1024,-5424.5998 1024,-5424.5998 1018,-5424.5998 1012,-5418.5998 1012,-5412.5998 1012,-5412.5998 1012,-5400.5998 1012,-5400.5998 1012,-5394.5998 1018,-5388.5998 1024,-5388.5998 1024,-5388.5998 1308,-5388.5998 1308,-5388.5998 1314,-5388.5998 1320,-5394.5998 1320,-5400.5998 1320,-5400.5998 1320,-5412.5998 1320,-5412.5998 1320,-5418.5998 1314,-5424.5998 1308,-5424.5998"/> +<text text-anchor="middle" x="1166" y="-5402.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache --> +<g id="edge254" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache</title> +<path fill="none" stroke="#000000" d="M439.1518,-5303.7728C476.1774,-5323.0942 538.4806,-5352.7357 596,-5366.5998 732.0075,-5399.3823 891.149,-5408.1848 1006.6599,-5409.4793"/> +<polygon fill="#000000" stroke="#000000" points="1006.7646,-5411.2303 1011.7824,-5409.5321 1006.8007,-5407.7305 1006.7646,-5411.2303"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory --> +<g id="node98" class="node"> +<title>github.com/docker/distribution/registry/storage/cache/memory</title> +<g id="a_node98"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" xlink:title="github.com/docker/distribution/registry/storage/cache/memory" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M942,-5244.5998C942,-5244.5998 608,-5244.5998 608,-5244.5998 602,-5244.5998 596,-5238.5998 596,-5232.5998 596,-5232.5998 596,-5220.5998 596,-5220.5998 596,-5214.5998 602,-5208.5998 608,-5208.5998 608,-5208.5998 942,-5208.5998 942,-5208.5998 948,-5208.5998 954,-5214.5998 954,-5220.5998 954,-5220.5998 954,-5232.5998 954,-5232.5998 954,-5238.5998 948,-5244.5998 942,-5244.5998"/> +<text text-anchor="middle" x="775" y="-5222.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache/memory</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache/memory --> +<g id="edge255" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache/memory</title> +<path fill="none" stroke="#000000" d="M518.7172,-5267.5773C562.5945,-5260.5617 612.8888,-5252.52 657.2374,-5245.4291"/> +<polygon fill="#000000" stroke="#000000" points="657.6768,-5247.1311 662.3378,-5244.6136 657.1241,-5243.675 657.6768,-5247.1311"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->crypto/tls --> +<g id="edge363" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->crypto/tls</title> +<path fill="none" stroke="#000000" d="M3373.5745,-18573.7868C3476.1787,-18671.6172 3961.9744,-19134.81 4073.2524,-19240.9106"/> +<polygon fill="#000000" stroke="#000000" points="4072.2253,-19242.3493 4077.0517,-19244.5331 4074.6405,-19239.8162 4072.2253,-19242.3493"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->crypto/x509 --> +<g id="edge364" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->crypto/x509</title> +<path fill="none" stroke="#000000" d="M3483.3293,-18570.8494C3643.7848,-18587.0161 3906.1908,-18603.282 3983,-18547.5998 4034.9867,-18509.9125 4078.1996,-18308.5882 4091.6546,-18239.7205"/> +<polygon fill="#000000" stroke="#000000" points="4093.4111,-18239.8547 4092.6453,-18234.613 4089.9752,-18239.1883 4093.4111,-18239.8547"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->encoding/pem --> +<g id="edge365" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->encoding/pem</title> +<path fill="none" stroke="#000000" d="M3401.7707,-18573.7207C3509.7075,-18612.1218 3780.5845,-18691.485 3983,-18606.5998 4027.5937,-18587.899 4063.4611,-18542.0668 4082.0717,-18514.2381"/> +<polygon fill="#000000" stroke="#000000" points="4083.6186,-18515.0711 4084.9091,-18509.9332 4080.6963,-18513.1449 4083.6186,-18515.0711"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->fmt --> +<g id="edge366" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->fmt</title> +<path fill="none" stroke="#000000" d="M3356.0388,-18537.5439C3370.0855,-18371.9419 3476.5955,-17099.5739 3522,-16065.5998 3525.4311,-15987.4641 3536.9031,-13315.8656 3580,-13250.5998 3688.2942,-13086.5993 3874.4457,-13232.4283 3983,-13068.5998 4020.4328,-13012.1069 4039.2078,-10697.3453 4041,-10629.5998 4061.248,-9864.2427 4090.3118,-8924.4937 4095.2682,-8765.0892"/> +<polygon fill="#000000" stroke="#000000" points="4097.0279,-8764.8014 4095.4343,-8759.7494 4093.5296,-8764.6926 4097.0279,-8764.8014"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->io/ioutil --> +<g id="edge368" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3355.8134,-18537.4132C3368.5972,-18362.2792 3472.6992,-16978.1431 3580,-16832.5998 3696.95,-16673.9682 3858.0211,-16799.986 3983,-16647.5998 4053.6859,-16561.4128 4001.5334,-16507.8451 4041,-16403.5998 4051.9435,-16374.6941 4069.361,-16343.684 4081.659,-16323.3294"/> +<polygon fill="#000000" stroke="#000000" points="4083.2506,-16324.0803 4084.3564,-16318.8995 4080.2612,-16322.26 4083.2506,-16324.0803"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->github.com/pkg/errors --> +<g id="edge367" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M3356.1227,-18537.5474C3370.922,-18371.9761 3482.8476,-17099.8295 3522,-16065.5998 3536.3253,-15687.1905 3498.6817,-9619.446 3580,-9249.5998 3618.0417,-9076.5811 3727.775,-8889.5526 3767.1897,-8826.0993"/> +<polygon fill="#000000" stroke="#000000" points="3768.7435,-8826.9149 3769.9041,-8821.7461 3765.7736,-8825.063 3768.7435,-8826.9149"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->os --> +<g id="edge369" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->os</title> +<path fill="none" stroke="#000000" d="M3397.0216,-18573.6226C3442.1421,-18592.5433 3515.6608,-18622.793 3580,-18646.5998 3761.3274,-18713.6946 3982.2468,-18784.6345 4063.7799,-18810.457"/> +<polygon fill="#000000" stroke="#000000" points="4063.6518,-18812.2519 4068.9468,-18812.0921 4064.7079,-18808.915 4063.6518,-18812.2519"/> +</g> +<!-- github.com/docker/go-connections/tlsconfig->runtime --> +<g id="edge370" class="edge"> +<title>github.com/docker/go-connections/tlsconfig->runtime</title> +<path fill="none" stroke="#000000" d="M3483.052,-18545.8831C3661.6425,-18531.8115 3965.0856,-18505.5899 3983,-18488.5998 3995.7912,-18476.4686 4075.3368,-17928.1843 4092.6683,-17807.8049"/> +<polygon fill="#000000" stroke="#000000" points="4094.4195,-17807.9213 4093.3996,-17802.723 4090.9552,-17807.4228 4094.4195,-17807.9213"/> +</g> +<!-- github.com/ghodss/yaml->bytes --> +<g id="edge382" class="edge"> +<title>github.com/ghodss/yaml->bytes</title> +<path fill="none" stroke="#000000" d="M3357.6848,-3743.7341C3377.6384,-3858.3398 3485.7565,-4494.1091 3522,-5017.5998 3527.968,-5103.8 3527.0023,-11170.3552 3580,-11238.5998 3693.6647,-11384.9647 3866.536,-11201.4525 3983,-11345.5998 4021.7885,-11393.6083 4087.611,-13439.2995 4095.2669,-13681.2906"/> +<polygon fill="#000000" stroke="#000000" points="4093.5197,-13681.4088 4095.4269,-13686.3509 4097.0179,-13681.2981 4093.5197,-13681.4088"/> +</g> +<!-- github.com/ghodss/yaml->encoding --> +<g id="edge383" class="edge"> +<title>github.com/ghodss/yaml->encoding</title> +<path fill="none" stroke="#000000" d="M3430.9033,-3732.1076C3461.2658,-3737.5606 3495.2559,-3747.5068 3522,-3765.5998 3558.9733,-3790.6131 3548.0111,-3816.4646 3580,-3847.5998 3734.3184,-3997.8002 3866.5746,-3935.4384 3983,-4116.5998 4070.3808,-4252.567 4012.032,-4316.5925 4041,-4475.5998 4057.7774,-4567.6922 4080.9045,-4676.7129 4090.9756,-4723.4492"/> +<polygon fill="#000000" stroke="#000000" points="4089.3153,-4724.0524 4092.0807,-4728.5707 4092.7366,-4723.3141 4089.3153,-4724.0524"/> +</g> +<!-- github.com/ghodss/yaml->encoding/json --> +<g id="edge384" class="edge"> +<title>github.com/ghodss/yaml->encoding/json</title> +<path fill="none" stroke="#000000" d="M3357.7155,-3743.732C3377.8564,-3858.325 3486.919,-4494.0299 3522,-5017.5998 3527.5155,-5099.9162 3529.2398,-16832.5628 3580,-16897.5998 3693.7437,-17043.335 3850.6225,-16871.5549 3983,-17000.5998 4050.8022,-17066.6951 3982.9614,-17130.785 4041,-17205.5998 4044.0979,-17209.5932 4047.8451,-17213.1812 4051.9106,-17216.3813"/> +<polygon fill="#000000" stroke="#000000" points="4051.0081,-17217.8903 4056.0699,-17219.4526 4053.0873,-17215.0747 4051.0081,-17217.8903"/> +</g> +<!-- github.com/ghodss/yaml->fmt --> +<g id="edge385" class="edge"> +<title>github.com/ghodss/yaml->fmt</title> +<path fill="none" stroke="#000000" d="M3357.5968,-3743.7405C3377.0127,-3858.3852 3482.4195,-4494.3508 3522,-5017.5998 3527.5055,-5090.3819 3530.9369,-7590.5595 3580,-7644.5998 3701.2475,-7778.1473 3856.173,-7564.3391 3983,-7692.5998 4020.4688,-7730.4922 4083.2076,-8566.5242 4094.2989,-8718.1326"/> +<polygon fill="#000000" stroke="#000000" points="4092.5619,-8718.3754 4094.6716,-8723.2346 4096.0526,-8718.1204 4092.5619,-8718.3754"/> +</g> +<!-- github.com/ghodss/yaml->reflect --> +<g id="edge387" class="edge"> +<title>github.com/ghodss/yaml->reflect</title> +<path fill="none" stroke="#000000" d="M3430.9172,-3726.8476C3462.9276,-3731.2734 3498.1617,-3741.8715 3522,-3765.5998 3599.1629,-3842.4068 3522.8089,-3910.9575 3580,-4003.5998 3703.4003,-4203.4926 3870.3813,-4139.4401 3983,-4345.5998 4076.2936,-4516.3831 4006.3392,-4588.1077 4041,-4779.5998 4055.5227,-4859.8339 4078.9383,-4953.7453 4089.932,-4996.4079"/> +<polygon fill="#000000" stroke="#000000" points="4088.2643,-4996.9491 4091.2096,-5001.3522 4091.6529,-4996.0734 4088.2643,-4996.9491"/> +</g> +<!-- github.com/ghodss/yaml->sort --> +<g id="edge388" class="edge"> +<title>github.com/ghodss/yaml->sort</title> +<path fill="none" stroke="#000000" d="M3430.7301,-3739.354C3590.0012,-3768.8 3946.3931,-3838.6648 3983,-3880.5998 4138.8325,-4059.1136 3911.8483,-4211.9273 4041,-4410.5998 4046.5375,-4419.1181 4055.1947,-4425.6459 4064.0154,-4430.5141"/> +<polygon fill="#000000" stroke="#000000" points="4063.669,-4432.3053 4068.9139,-4433.05 4065.2782,-4429.1971 4063.669,-4432.3053"/> +</g> +<!-- github.com/ghodss/yaml->strconv --> +<g id="edge389" class="edge"> +<title>github.com/ghodss/yaml->strconv</title> +<path fill="none" stroke="#000000" d="M3430.9265,-3725.02C3463.7379,-3728.974 3499.6074,-3739.7068 3522,-3765.5998 3616.1368,-3874.4521 3481.1116,-4306.0453 3580,-4410.5998 3704.3876,-4542.1146 3857.9906,-4338.676 3983,-4469.5998 4072.9104,-4563.764 4022.7631,-4923.6883 4041,-5052.5998 4055.6232,-5155.967 4080.4823,-5278.0949 4091.0308,-5328.2878"/> +<polygon fill="#000000" stroke="#000000" points="4089.3823,-5328.9518 4092.1257,-5333.4835 4092.8071,-5328.2301 4089.3823,-5328.9518"/> +</g> +<!-- github.com/ghodss/yaml->strings --> +<g id="edge390" class="edge"> +<title>github.com/ghodss/yaml->strings</title> +<path fill="none" stroke="#000000" d="M3356.8213,-3743.7644C3379.744,-3923.0072 3565.0344,-5368.7324 3580,-5384.5998 3705.233,-5517.3789 3864.0207,-5325.1892 3983,-5463.5998 4032.0077,-5520.6113 4031.6347,-6741.0052 4041,-6815.5998 4052.2056,-6904.8523 4077.904,-7008.9017 4089.7768,-7054.3609"/> +<polygon fill="#000000" stroke="#000000" points="4088.1245,-7054.9594 4091.0863,-7059.3515 4091.5099,-7054.0711 4088.1245,-7054.9594"/> +</g> +<!-- github.com/ghodss/yaml->sync --> +<g id="edge391" class="edge"> +<title>github.com/ghodss/yaml->sync</title> +<path fill="none" stroke="#000000" d="M3361.5592,-3707.3003C3398.4266,-3611.8307 3567.6394,-3174.942 3580,-3166.5998 3734.2742,-3062.48 3977.6165,-3141.674 4064.1565,-3175.3597"/> +<polygon fill="#000000" stroke="#000000" points="4063.6012,-3177.0217 4068.8949,-3177.2199 4064.8802,-3173.7637 4063.6012,-3177.0217"/> +</g> +<!-- github.com/ghodss/yaml->unicode --> +<g id="edge392" class="edge"> +<title>github.com/ghodss/yaml->unicode</title> +<path fill="none" stroke="#000000" d="M3430.7726,-3711.2584C3579.0183,-3685.2192 3898.8842,-3638.5807 3983,-3703.5998 3999.9116,-3716.672 4070.975,-4026.7429 4090.8284,-4114.5949"/> +<polygon fill="#000000" stroke="#000000" points="4089.1448,-4115.0845 4091.9531,-4119.5763 4092.5589,-4114.3137 4089.1448,-4115.0845"/> +</g> +<!-- github.com/ghodss/yaml->unicode/utf8 --> +<g id="edge393" class="edge"> +<title>github.com/ghodss/yaml->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3430.6198,-3718.6505C3580.1432,-3706.7713 3904.5023,-3690.3184 3983,-3762.5998 4050.3681,-3824.633 4022.7653,-4080.8554 4041,-4170.5998 4054.7466,-4238.2555 4077.4831,-4316.6561 4088.9391,-4354.63"/> +<polygon fill="#000000" stroke="#000000" points="4087.2884,-4355.2173 4090.4121,-4359.4957 4090.6382,-4354.2031 4087.2884,-4355.2173"/> +</g> +<!-- gopkg.in/yaml.v2 --> +<g id="node109" class="node"> +<title>gopkg.in/yaml.v2</title> +<g id="a_node109"><a xlink:href="https://godoc.org/gopkg.in/yaml.v2" xlink:title="gopkg.in/yaml.v2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3826,-4395.5998C3826,-4395.5998 3737,-4395.5998 3737,-4395.5998 3731,-4395.5998 3725,-4389.5998 3725,-4383.5998 3725,-4383.5998 3725,-4371.5998 3725,-4371.5998 3725,-4365.5998 3731,-4359.5998 3737,-4359.5998 3737,-4359.5998 3826,-4359.5998 3826,-4359.5998 3832,-4359.5998 3838,-4365.5998 3838,-4371.5998 3838,-4371.5998 3838,-4383.5998 3838,-4383.5998 3838,-4389.5998 3832,-4395.5998 3826,-4395.5998"/> +<text text-anchor="middle" x="3781.5" y="-4373.8998" font-family="Times,serif" font-size="14.00" fill="#000000">gopkg.in/yaml.v2</text> +</a> +</g> +</g> +<!-- github.com/ghodss/yaml->gopkg.in/yaml.v2 --> +<g id="edge386" class="edge"> +<title>github.com/ghodss/yaml->gopkg.in/yaml.v2</title> +<path fill="none" stroke="#000000" d="M3430.6971,-3725.6588C3463.2583,-3729.7731 3498.9908,-3740.4554 3522,-3765.5998 3641.7867,-3896.5021 3503.2865,-4000.602 3580,-4160.5998 3621.1019,-4246.3242 3707.9623,-4321.4294 3753.0289,-4356.528"/> +<polygon fill="#000000" stroke="#000000" points="3751.9676,-4357.9194 3756.9928,-4359.5958 3754.1098,-4355.1515 3751.9676,-4357.9194"/> +</g> +<!-- github.com/opencontainers/go-digest->crypto --> +<g id="edge485" class="edge"> +<title>github.com/opencontainers/go-digest->crypto</title> +<path fill="none" stroke="#000000" d="M3824.7825,-11040.6434C3873.3785,-11063.541 3949.7742,-11108.1038 3983,-11173.5998 4112.6991,-11429.2682 3969.2726,-16056.033 4041,-16333.5998 4048.6279,-16363.1178 4066.4974,-16393.4307 4079.8106,-16413.2542"/> +<polygon fill="#000000" stroke="#000000" points="4078.4863,-16414.4187 4082.7466,-16417.5671 4081.3795,-16412.4491 4078.4863,-16414.4187"/> +</g> +<!-- github.com/opencontainers/go-digest->fmt --> +<g id="edge486" class="edge"> +<title>github.com/opencontainers/go-digest->fmt</title> +<path fill="none" stroke="#000000" d="M3801.427,-11004.5379C3844.0343,-10964.4832 3943.3362,-10863.1296 3983,-10754.5998 4054.7528,-10558.2669 4090.9874,-8975.6349 4095.514,-8764.7813"/> +<polygon fill="#000000" stroke="#000000" points="4097.2636,-8764.8128 4095.621,-8759.7765 4093.7644,-8764.738 4097.2636,-8764.8128"/> +</g> +<!-- github.com/opencontainers/go-digest->io --> +<g id="edge488" class="edge"> +<title>github.com/opencontainers/go-digest->io</title> +<path fill="none" stroke="#000000" d="M3824.6506,-11040.7108C3873.1227,-11063.6717 3949.4022,-11108.2939 3983,-11173.5998 4048.1816,-11300.2969 4013.5694,-13597.7844 4041,-13737.5998 4049.7667,-13782.2843 4071.1779,-13831.1854 4084.5516,-13858.9566"/> +<polygon fill="#000000" stroke="#000000" points="4083.0288,-13859.8269 4086.788,-13863.5594 4086.1769,-13858.2973 4083.0288,-13859.8269"/> +</g> +<!-- github.com/opencontainers/go-digest->strings --> +<g id="edge490" class="edge"> +<title>github.com/opencontainers/go-digest->strings</title> +<path fill="none" stroke="#000000" d="M3802.0465,-11004.3395C3845.4065,-10964.2826 3945.3612,-10863.5405 3983,-10754.5998 4034.0263,-10606.9107 4034.4297,-8098.7171 4041,-7942.5998 4055.0794,-7608.057 4086.237,-7201.5575 4094.1462,-7100.9503"/> +<polygon fill="#000000" stroke="#000000" points="4095.9002,-7100.9679 4094.5482,-7095.8459 4092.411,-7100.693 4095.9002,-7100.9679"/> +</g> +<!-- github.com/opencontainers/go-digest->regexp --> +<g id="edge489" class="edge"> +<title>github.com/opencontainers/go-digest->regexp</title> +<path fill="none" stroke="#000000" d="M3802.1592,-11004.3779C3845.7379,-10964.3959 3946.0927,-10863.7904 3983,-10754.5998 4054.5512,-10542.9151 4025.4155,-2933.5059 4041,-2710.5998 4051.4519,-2561.1054 4081.0082,-2383.0887 4091.9019,-2320.6239"/> +<polygon fill="#000000" stroke="#000000" points="4093.6372,-2320.8598 4092.7756,-2315.6329 4090.1896,-2320.2563 4093.6372,-2320.8598"/> +</g> +<!-- hash --> +<g id="node119" class="node"> +<title>hash</title> +<g id="a_node119"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-11846.5998C4111,-11846.5998 4081,-11846.5998 4081,-11846.5998 4075,-11846.5998 4069,-11840.5998 4069,-11834.5998 4069,-11834.5998 4069,-11822.5998 4069,-11822.5998 4069,-11816.5998 4075,-11810.5998 4081,-11810.5998 4081,-11810.5998 4111,-11810.5998 4111,-11810.5998 4117,-11810.5998 4123,-11816.5998 4123,-11822.5998 4123,-11822.5998 4123,-11834.5998 4123,-11834.5998 4123,-11840.5998 4117,-11846.5998 4111,-11846.5998"/> +<text text-anchor="middle" x="4096" y="-11824.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/go-digest->hash --> +<g id="edge487" class="edge"> +<title>github.com/opencontainers/go-digest->hash</title> +<path fill="none" stroke="#000000" d="M3820.8945,-11040.6059C3867.6276,-11064.1562 3944.0885,-11110.0287 3983,-11173.5998 4016.9537,-11229.0712 4078.7401,-11694.813 4093.0059,-11805.2037"/> +<polygon fill="#000000" stroke="#000000" points="4091.312,-11805.752 4093.6874,-11810.487 4094.7833,-11805.3042 4091.312,-11805.752"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->time --> +<g id="edge494" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->time</title> +<path fill="none" stroke="#000000" d="M3412.3399,-11187.636C3457.8381,-11201.7418 3522.8713,-11221.7237 3580,-11238.5998 3762.473,-11292.5033 3982.269,-11353.9757 4063.6536,-11376.6201"/> +<polygon fill="#000000" stroke="#000000" points="4063.5262,-11378.401 4068.8123,-11378.055 4064.4641,-11375.029 4063.5262,-11378.401"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest --> +<g id="edge492" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M3385.9519,-11151.5989C3428.6133,-11127.9096 3507.7993,-11086.4865 3580,-11062.5998 3607.7674,-11053.4133 3638.422,-11046.0077 3667.2645,-11040.1848"/> +<polygon fill="#000000" stroke="#000000" points="3667.6902,-11041.8845 3672.2512,-11039.19 3667.0054,-11038.4521 3667.6902,-11041.8845"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go --> +<g id="edge493" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go</title> +<path fill="none" stroke="#000000" d="M3358.1079,-11151.3872C3377.345,-11053.7669 3468.9535,-10582.409 3522,-10193.5998 3539.2412,-10067.2287 3507.1787,-9727.3088 3580,-9622.5998 3607.9105,-9582.4676 3656.8358,-9557.8966 3699.4866,-9543.2718"/> +<polygon fill="#000000" stroke="#000000" points="3700.206,-9544.8763 3704.3883,-9541.6249 3699.0912,-9541.5586 3700.206,-9544.8763"/> +</g> +<!-- github.com/pkg/errors->fmt --> +<g id="edge504" class="edge"> +<title>github.com/pkg/errors->fmt</title> +<path fill="none" stroke="#000000" d="M3850.5184,-8789.9937C3916.7726,-8776.9324 4013.9231,-8757.7803 4063.8147,-8747.9448"/> +<polygon fill="#000000" stroke="#000000" points="4064.2176,-8749.6491 4068.7846,-8746.965 4063.5406,-8746.2152 4064.2176,-8749.6491"/> +</g> +<!-- github.com/pkg/errors->io --> +<g id="edge505" class="edge"> +<title>github.com/pkg/errors->io</title> +<path fill="none" stroke="#000000" d="M3793.0635,-8821.7533C3829.7867,-8880.5958 3943.6544,-9072.5151 3983,-9249.5998 4091.1678,-9736.4362 3947.3612,-13247.7613 4041,-13737.5998 4049.55,-13782.3262 4071.0326,-13831.2135 4084.4785,-13858.9708"/> +<polygon fill="#000000" stroke="#000000" points="4082.9592,-13859.8477 4086.7274,-13863.5711 4086.1036,-13858.3105 4082.9592,-13859.8477"/> +</g> +<!-- github.com/pkg/errors->strings --> +<g id="edge508" class="edge"> +<title>github.com/pkg/errors->strings</title> +<path fill="none" stroke="#000000" d="M3786.0781,-8785.2961C3809.5237,-8691.0898 3917.5809,-8251.1938 3983,-7886.5998 4038.4919,-7577.332 4082.5723,-7197.4133 4093.428,-7100.7921"/> +<polygon fill="#000000" stroke="#000000" points="4095.19,-7100.7821 4094.0079,-7095.6183 4091.7118,-7100.3922 4095.19,-7100.7821"/> +</g> +<!-- github.com/pkg/errors->path --> +<g id="edge506" class="edge"> +<title>github.com/pkg/errors->path</title> +<path fill="none" stroke="#000000" d="M3786.9551,-8785.425C3814.73,-8691.8549 3940.7605,-8254.6002 3983,-7886.5998 4102.8698,-6842.2666 3965.2023,-4205.0536 4041,-3156.5998 4051.8316,-3006.7749 4081.153,-2828.3077 4091.9434,-2765.6831"/> +<polygon fill="#000000" stroke="#000000" points="4093.681,-2765.9045 4092.8086,-2760.6794 4090.2322,-2765.308 4093.681,-2765.9045"/> +</g> +<!-- github.com/pkg/errors->runtime --> +<g id="edge507" class="edge"> +<title>github.com/pkg/errors->runtime</title> +<path fill="none" stroke="#000000" d="M3793.1052,-8821.7441C3829.9541,-8880.5589 3944.1585,-9072.4038 3983,-9249.5998 4078.4154,-9684.8877 4015.6513,-16825.6986 4041,-17270.5998 4051.8216,-17460.5321 4082.4323,-17688.3443 4092.6401,-17761.0781"/> +<polygon fill="#000000" stroke="#000000" points="4090.9655,-17761.7365 4093.3957,-17766.4436 4094.4313,-17761.2484 4090.9655,-17761.7365"/> +</g> +<!-- github.com/sirupsen/logrus->bufio --> +<g id="edge609" class="edge"> +<title>github.com/sirupsen/logrus->bufio</title> +<path fill="none" stroke="#000000" d="M3355.7505,-12323.7332C3362.5248,-12409.6141 3401.4474,-12780.6708 3580,-13006.5998 3709.8266,-13170.8741 3872.356,-13072.839 3983,-13250.5998 4139.5394,-13502.0963 3882.2685,-13664.4811 4041,-13914.5998 4046.444,-13923.1781 4055.0749,-13929.7229 4063.9003,-13934.5881"/> +<polygon fill="#000000" stroke="#000000" points="4063.5587,-13936.3812 4068.8043,-13937.1204 4065.1646,-13933.2713 4063.5587,-13936.3812"/> +</g> +<!-- github.com/sirupsen/logrus->bytes --> +<g id="edge610" class="edge"> +<title>github.com/sirupsen/logrus->bytes</title> +<path fill="none" stroke="#000000" d="M3423.2728,-12323.6273C3582.5685,-12365.6799 3964.7141,-12468.384 3983,-12490.5998 4061.604,-12586.0968 4090.8705,-13519.1731 4095.3678,-13680.8019"/> +<polygon fill="#000000" stroke="#000000" points="4093.6301,-13681.2736 4095.5174,-13686.2234 4097.1287,-13681.177 4093.6301,-13681.2736"/> +</g> +<!-- github.com/sirupsen/logrus->context --> +<g id="edge611" class="edge"> +<title>github.com/sirupsen/logrus->context</title> +<path fill="none" stroke="#000000" d="M3401.3843,-12287.5368C3441.515,-12269.272 3496.6954,-12236.8115 3522,-12188.5998 3655.4729,-11934.3003 3488.6181,-2121.8728 3580,-1849.5998 3675.1722,-1566.0334 3848.3425,-1577.686 3983,-1310.5998 4046.1483,-1185.3483 4080.7916,-1018.4971 4091.9519,-957.8643"/> +<polygon fill="#000000" stroke="#000000" points="4093.7239,-957.9014 4092.8984,-952.6687 4090.2806,-957.2741 4093.7239,-957.9014"/> +</g> +<!-- github.com/sirupsen/logrus->encoding/json --> +<g id="edge612" class="edge"> +<title>github.com/sirupsen/logrus->encoding/json</title> +<path fill="none" stroke="#000000" d="M3358.848,-12323.783C3381.4476,-12419.286 3485.8409,-12872.85 3522,-13250.5998 3531.9454,-13354.4984 3516.1115,-16925.0644 3580,-17007.5998 3693.7192,-17154.5097 3832.4235,-17009.7821 3983,-17118.5998 4020.6654,-17145.8197 4007.1519,-17173.7578 4041,-17205.5998 4045.2746,-17209.6211 4050.1436,-17213.3514 4055.1763,-17216.7384"/> +<polygon fill="#000000" stroke="#000000" points="4054.286,-17218.2469 4059.4327,-17219.5016 4056.1918,-17215.3112 4054.286,-17218.2469"/> +</g> +<!-- github.com/sirupsen/logrus->fmt --> +<g id="edge613" class="edge"> +<title>github.com/sirupsen/logrus->fmt</title> +<path fill="none" stroke="#000000" d="M3404.4284,-12287.5069C3449.683,-12271.7871 3518.5067,-12249.5617 3580,-12236.5998 3668.2494,-12217.9982 3922.02,-12255.0484 3983,-12188.5998 3998.4074,-12171.8107 4086.7886,-9066.6037 4095.3355,-8765.075"/> +<polygon fill="#000000" stroke="#000000" points="4097.0971,-8764.6887 4095.4895,-8759.6411 4093.5985,-8764.5895 4097.0971,-8764.6887"/> +</g> +<!-- github.com/sirupsen/logrus->io --> +<g id="edge615" class="edge"> +<title>github.com/sirupsen/logrus->io</title> +<path fill="none" stroke="#000000" d="M3364.6989,-12323.6917C3394.7663,-12376.4581 3486.805,-12533.9463 3580,-12652.5998 3741.387,-12858.0737 3876.0595,-12842.2115 3983,-13080.5998 4102.9806,-13348.0566 3975.1858,-13451.948 4041,-13737.5998 4051.2237,-13781.9736 4072.155,-13830.977 4085.0431,-13858.8518"/> +<polygon fill="#000000" stroke="#000000" points="4083.4978,-13859.679 4087.1955,-13863.4725 4086.6705,-13858.2011 4083.4978,-13859.679"/> +</g> +<!-- github.com/sirupsen/logrus->reflect --> +<g id="edge618" class="edge"> +<title>github.com/sirupsen/logrus->reflect</title> +<path fill="none" stroke="#000000" d="M3401.3555,-12287.5217C3441.4672,-12269.2468 3496.6356,-12236.7801 3522,-12188.5998 3614.7824,-12012.3574 3438.5948,-5158.8658 3580,-5018.5998 3647.9394,-4951.2078 3961.8574,-4996.9955 4063.8766,-5013.9921"/> +<polygon fill="#000000" stroke="#000000" points="4063.6395,-5015.7266 4068.86,-5014.8276 4064.2182,-5012.2748 4063.6395,-5015.7266"/> +</g> +<!-- github.com/sirupsen/logrus->sort --> +<g id="edge620" class="edge"> +<title>github.com/sirupsen/logrus->sort</title> +<path fill="none" stroke="#000000" d="M3401.3636,-12287.5259C3441.4806,-12269.2539 3496.6524,-12236.7889 3522,-12188.5998 3572.7119,-12092.1902 3502.6881,-4422.3421 3580,-4345.5998 3643.5591,-4282.5091 3901.2689,-4308.9907 3983,-4345.5998 4018.335,-4361.4271 4011.0064,-4386.1163 4041,-4410.5998 4048.1486,-4416.4351 4056.4878,-4421.8694 4064.4796,-4426.543"/> +<polygon fill="#000000" stroke="#000000" points="4063.6653,-4428.0932 4068.874,-4429.0589 4065.4043,-4425.0558 4063.6653,-4428.0932"/> +</g> +<!-- github.com/sirupsen/logrus->strings --> +<g id="edge621" class="edge"> +<title>github.com/sirupsen/logrus->strings</title> +<path fill="none" stroke="#000000" d="M3400.7584,-12287.5904C3440.7633,-12269.275 3496.1211,-12236.7071 3522,-12188.5998 3639.4231,-11970.3175 3453.3539,-10171.6635 3580,-9958.5998 3685.7128,-9780.7534 3876.7084,-9903.1009 3983,-9725.5998 4033.9169,-9640.5713 4036.2438,-8041.5936 4041,-7942.5998 4057.0688,-7608.1466 4086.7574,-7201.581 4094.2483,-7100.9549"/> +<polygon fill="#000000" stroke="#000000" points="4096.0023,-7100.9658 4094.6289,-7095.8495 4092.5119,-7100.7056 4096.0023,-7100.9658"/> +</g> +<!-- github.com/sirupsen/logrus->sync --> +<g id="edge622" class="edge"> +<title>github.com/sirupsen/logrus->sync</title> +<path fill="none" stroke="#000000" d="M3401.3713,-12287.53C3441.4934,-12269.2606 3496.6683,-12236.7973 3522,-12188.5998 3633.3492,-11976.7404 3433.4459,-3761.8217 3580,-3572.5998 3693.4749,-3426.0878 3843.8602,-3588.0025 3983,-3465.5998 4061.4187,-3396.6141 4086.3642,-3265.0864 4093.4574,-3211.8404"/> +<polygon fill="#000000" stroke="#000000" points="4095.2245,-3211.8195 4094.1265,-3206.6371 4091.753,-3211.373 4095.2245,-3211.8195"/> +</g> +<!-- github.com/sirupsen/logrus->time --> +<g id="edge624" class="edge"> +<title>github.com/sirupsen/logrus->time</title> +<path fill="none" stroke="#000000" d="M3402.572,-12323.7427C3518.9264,-12364.1999 3818.2401,-12447.074 3983,-12298.5998 4066.4223,-12223.4234 4027.1075,-11908.0348 4041,-11796.5998 4059.451,-11648.5992 4084.0594,-11470.9684 4092.7748,-11408.5964"/> +<polygon fill="#000000" stroke="#000000" points="4094.5123,-11408.8068 4093.4717,-11403.6126 4091.046,-11408.3221 4094.5123,-11408.8068"/> +</g> +<!-- github.com/sirupsen/logrus->os --> +<g id="edge617" class="edge"> +<title>github.com/sirupsen/logrus->os</title> +<path fill="none" stroke="#000000" d="M3358.8599,-12323.7819C3381.5194,-12419.2792 3486.1663,-12872.819 3522,-13250.5998 3533.0939,-13367.5581 3502.2495,-17391.525 3580,-17479.5998 3699.6748,-17615.166 3861.1353,-17401.9987 3983,-17535.5998 4076.9238,-17638.5691 3968.7409,-18669.4236 4041,-18788.5998 4046.4143,-18797.5296 4055.3296,-18804.2279 4064.4219,-18809.1251"/> +<polygon fill="#000000" stroke="#000000" points="4063.634,-18810.6876 4068.8851,-18811.3864 4065.2158,-18807.5655 4063.634,-18810.6876"/> +</g> +<!-- golang.org/x/sys/unix --> +<g id="node70" class="node"> +<title>golang.org/x/sys/unix</title> +<g id="a_node70"><a xlink:href="https://godoc.org/golang.org/x/sys/unix" xlink:title="golang.org/x/sys/unix" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3836.5,-14020.5998C3836.5,-14020.5998 3726.5,-14020.5998 3726.5,-14020.5998 3720.5,-14020.5998 3714.5,-14014.5998 3714.5,-14008.5998 3714.5,-14008.5998 3714.5,-13996.5998 3714.5,-13996.5998 3714.5,-13990.5998 3720.5,-13984.5998 3726.5,-13984.5998 3726.5,-13984.5998 3836.5,-13984.5998 3836.5,-13984.5998 3842.5,-13984.5998 3848.5,-13990.5998 3848.5,-13996.5998 3848.5,-13996.5998 3848.5,-14008.5998 3848.5,-14008.5998 3848.5,-14014.5998 3842.5,-14020.5998 3836.5,-14020.5998"/> +<text text-anchor="middle" x="3781.5" y="-13998.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/sys/unix</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->golang.org/x/sys/unix --> +<g id="edge614" class="edge"> +<title>github.com/sirupsen/logrus->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M3357.5736,-12323.8354C3375.2734,-12427.9278 3467.3563,-12957.209 3580,-13380.5998 3643.1122,-13617.8179 3743.3839,-13898.1077 3772.9999,-13979.4278"/> +<polygon fill="#000000" stroke="#000000" points="3771.4902,-13980.3961 3774.8474,-13984.4939 3774.7784,-13979.197 3771.4902,-13980.3961"/> +</g> +<!-- github.com/sirupsen/logrus->runtime --> +<g id="edge619" class="edge"> +<title>github.com/sirupsen/logrus->runtime</title> +<path fill="none" stroke="#000000" d="M3358.8528,-12323.7826C3381.4766,-12419.2832 3485.972,-12872.8375 3522,-13250.5998 3542.7522,-13468.1918 3481.9994,-16989.2212 3580,-17184.5998 3681.3153,-17386.5867 3847.4478,-17313.7986 3983,-17494.5998 4047.7108,-17580.9119 4080.2153,-17708.9733 4091.3905,-17761.1739"/> +<polygon fill="#000000" stroke="#000000" points="4089.7228,-17761.7475 4092.4668,-17766.2789 4093.1476,-17761.0254 4089.7228,-17761.7475"/> +</g> +<!-- log --> +<g id="node111" class="node"> +<title>log</title> +<g id="a_node111"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-11338.5998C4111,-11338.5998 4081,-11338.5998 4081,-11338.5998 4075,-11338.5998 4069,-11332.5998 4069,-11326.5998 4069,-11326.5998 4069,-11314.5998 4069,-11314.5998 4069,-11308.5998 4075,-11302.5998 4081,-11302.5998 4081,-11302.5998 4111,-11302.5998 4111,-11302.5998 4117,-11302.5998 4123,-11308.5998 4123,-11314.5998 4123,-11314.5998 4123,-11326.5998 4123,-11326.5998 4123,-11332.5998 4117,-11338.5998 4111,-11338.5998"/> +<text text-anchor="middle" x="4096" y="-11316.8998" font-family="Times,serif" font-size="14.00" fill="#000000">log</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->log --> +<g id="edge616" class="edge"> +<title>github.com/sirupsen/logrus->log</title> +<path fill="none" stroke="#000000" d="M3437.228,-12299.7326C3603.5619,-12287.5517 3964.3103,-12259.0381 3983,-12239.5998 4119.7541,-12097.3687 3938.2264,-11522.0307 4041,-11353.5998 4046.483,-11344.614 4055.4162,-11337.7647 4064.5039,-11332.6994"/> +<polygon fill="#000000" stroke="#000000" points="4065.3521,-11334.2306 4068.9631,-11330.3545 4063.7231,-11331.1328 4065.3521,-11334.2306"/> +</g> +<!-- sync/atomic --> +<g id="node112" class="node"> +<title>sync/atomic</title> +<g id="a_node112"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4125.5,-7992.5998C4125.5,-7992.5998 4066.5,-7992.5998 4066.5,-7992.5998 4060.5,-7992.5998 4054.5,-7986.5998 4054.5,-7980.5998 4054.5,-7980.5998 4054.5,-7968.5998 4054.5,-7968.5998 4054.5,-7962.5998 4060.5,-7956.5998 4066.5,-7956.5998 4066.5,-7956.5998 4125.5,-7956.5998 4125.5,-7956.5998 4131.5,-7956.5998 4137.5,-7962.5998 4137.5,-7968.5998 4137.5,-7968.5998 4137.5,-7980.5998 4137.5,-7980.5998 4137.5,-7986.5998 4131.5,-7992.5998 4125.5,-7992.5998"/> +<text text-anchor="middle" x="4096" y="-7970.8998" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text> +</a> +</g> +</g> +<!-- github.com/sirupsen/logrus->sync/atomic --> +<g id="edge623" class="edge"> +<title>github.com/sirupsen/logrus->sync/atomic</title> +<path fill="none" stroke="#000000" d="M3400.6761,-12287.5459C3440.6257,-12269.2007 3495.9484,-12236.6139 3522,-12188.5998 3614.6236,-12017.8909 3510.5286,-10622.9679 3580,-10441.5998 3679.6379,-10181.4764 3881.6598,-10221.0648 3983,-9961.5998 4033.6642,-9831.8825 4032.581,-8848.6054 4041,-8709.5998 4057.9899,-8429.0797 4086.3087,-8088.9145 4094.0133,-7997.91"/> +<polygon fill="#000000" stroke="#000000" points="4095.7703,-7997.9005 4094.449,-7992.7705 4092.2828,-7997.6048 4095.7703,-7997.9005"/> +</g> +<!-- github.com/containers/image/v5/internal/pkg/keyctl->unsafe --> +<g id="edge87" class="edge"> +<title>github.com/containers/image/v5/internal/pkg/keyctl->unsafe</title> +<path fill="none" stroke="#000000" d="M3445.9938,-16050.6363C3584.8255,-16072.0854 3846.8972,-16088.2934 3983,-15939.5998 4050.6682,-15865.6716 4090.5142,-14247.6762 4095.4737,-14034.6683"/> +<polygon fill="#000000" stroke="#000000" points="4097.2244,-14034.6558 4095.5909,-14029.6165 4093.7253,-14034.5745 4097.2244,-14034.6558"/> +</g> +<!-- github.com/containers/image/v5/internal/pkg/keyctl->golang.org/x/sys/unix --> +<g id="edge86" class="edge"> +<title>github.com/containers/image/v5/internal/pkg/keyctl->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M3361.6894,-16014.5234C3388.8283,-15945.3 3485.3618,-15689.904 3522,-15469.5998 3535.0803,-15390.9483 3527.1601,-14095.3084 3580,-14035.5998 3611.4428,-14000.0697 3665.0859,-13992.671 3709.1211,-13993.7003"/> +<polygon fill="#000000" stroke="#000000" points="3709.1163,-13995.4509 3714.1678,-13993.8557 3709.2241,-13991.9525 3709.1163,-13995.4509"/> +</g> +<!-- golang.org/x/sys/unix->bytes --> +<g id="edge663" class="edge"> +<title>golang.org/x/sys/unix->bytes</title> +<path fill="none" stroke="#000000" d="M3808.4978,-13984.4087C3848.6999,-13956.6846 3925.7044,-13901.0933 3983,-13844.5998 4021.5823,-13806.5577 4059.7618,-13755.789 4080.369,-13727.0085"/> +<polygon fill="#000000" stroke="#000000" points="4081.8021,-13728.013 4083.2799,-13722.9258 4078.9523,-13725.9811 4081.8021,-13728.013"/> +</g> +<!-- golang.org/x/sys/unix->encoding/binary --> +<g id="edge664" class="edge"> +<title>golang.org/x/sys/unix->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3807.4023,-13984.3824C3852.5084,-13950.9654 3944.3493,-13875.0286 3983,-13785.5998 4051.3925,-13627.3555 4090.0447,-12328.2709 4095.3649,-12137.861"/> +<polygon fill="#000000" stroke="#000000" points="4097.1158,-12137.8522 4095.5056,-12132.8054 4093.6171,-12137.7547 4097.1158,-12137.8522"/> +</g> +<!-- golang.org/x/sys/unix->sort --> +<g id="edge667" class="edge"> +<title>golang.org/x/sys/unix->sort</title> +<path fill="none" stroke="#000000" d="M3789.7495,-13984.289C3822.4513,-13910.5834 3943.1265,-13627.6954 3983,-13380.5998 4059.6996,-12905.2933 3991.4478,-5193.4983 4041,-4714.5998 4050.6641,-4621.2004 4077.4919,-4512.3608 4089.7477,-4465.7263"/> +<polygon fill="#000000" stroke="#000000" points="4091.4427,-4466.1612 4091.0278,-4460.8801 4088.0588,-4465.2673 4091.4427,-4466.1612"/> +</g> +<!-- golang.org/x/sys/unix->strings --> +<g id="edge668" class="edge"> +<title>golang.org/x/sys/unix->strings</title> +<path fill="none" stroke="#000000" d="M3789.7212,-13984.2844C3822.3152,-13910.5613 3942.6358,-13627.6158 3983,-13380.5998 4080.4476,-12784.2526 4018.5982,-8546.441 4041,-7942.5998 4053.4136,-7607.991 4085.8013,-7201.5403 4094.0607,-7100.9469"/> +<polygon fill="#000000" stroke="#000000" points="4095.8147,-7100.9699 4094.4807,-7095.8432 4092.3265,-7100.6828 4095.8147,-7100.9699"/> +</g> +<!-- golang.org/x/sys/unix->sync --> +<g id="edge669" class="edge"> +<title>golang.org/x/sys/unix->sync</title> +<path fill="none" stroke="#000000" d="M3789.754,-13984.2897C3822.4729,-13910.5869 3943.2045,-13627.708 3983,-13380.5998 4067.5257,-12855.7416 4015.9924,-4342.6322 4041,-3811.5998 4052.0579,-3576.7872 4083.7482,-3293.5677 4093.2652,-3211.7517"/> +<polygon fill="#000000" stroke="#000000" points="4095.0183,-3211.8256 4093.8595,-3206.6565 4091.5419,-3211.4201 4095.0183,-3211.8256"/> +</g> +<!-- golang.org/x/sys/unix->time --> +<g id="edge671" class="edge"> +<title>golang.org/x/sys/unix->time</title> +<path fill="none" stroke="#000000" d="M3807.7511,-13984.5291C3853.4034,-13951.342 3946.0719,-13875.7536 3983,-13785.5998 4066.8048,-13581.0044 4023.0909,-12016.9672 4041,-11796.5998 4053.0528,-11648.2926 4081.4957,-11471.5502 4092.0058,-11408.9648"/> +<polygon fill="#000000" stroke="#000000" points="4093.7439,-11409.1816 4092.8489,-11403.9603 4090.2925,-11408.6 4093.7439,-11409.1816"/> +</g> +<!-- golang.org/x/sys/unix->unsafe --> +<g id="edge672" class="edge"> +<title>golang.org/x/sys/unix->unsafe</title> +<path fill="none" stroke="#000000" d="M3848.5824,-14004.5195C3914.8585,-14006.4161 4013.4105,-14009.2364 4063.7959,-14010.6782"/> +<polygon fill="#000000" stroke="#000000" points="4063.7654,-14012.428 4068.8135,-14010.8218 4063.8656,-14008.9294 4063.7654,-14012.428"/> +</g> +<!-- golang.org/x/sys/unix->runtime --> +<g id="edge666" class="edge"> +<title>golang.org/x/sys/unix->runtime</title> +<path fill="none" stroke="#000000" d="M3848.6081,-13993.6469C3893.7199,-13991.7624 3950.6465,-13998.2014 3983,-14035.5998 4041.8015,-14103.5704 4035.4077,-17180.8984 4041,-17270.5998 4052.8372,-17460.4715 4082.7816,-17688.3234 4092.7302,-17761.0728"/> +<polygon fill="#000000" stroke="#000000" points="4091.053,-17761.7236 4093.4664,-17766.4394 4094.5206,-17761.2479 4091.053,-17761.7236"/> +</g> +<!-- golang.org/x/sys/unix->net --> +<g id="edge665" class="edge"> +<title>golang.org/x/sys/unix->net</title> +<path fill="none" stroke="#000000" d="M3848.5993,-13993.6545C3893.7071,-13991.7734 3950.6325,-13998.2135 3983,-14035.5998 4095.9275,-14166.0376 3953.592,-16991.8501 4041,-17140.5998 4046.2907,-17149.6035 4055.1736,-17156.321 4064.2744,-17161.2132"/> +<polygon fill="#000000" stroke="#000000" points="4063.4926,-17162.7788 4068.7447,-17163.4702 4065.0701,-17159.6545 4063.4926,-17162.7788"/> +</g> +<!-- syscall --> +<g id="node103" class="node"> +<title>syscall</title> +<g id="a_node103"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4111,-18773.5998C4111,-18773.5998 4081,-18773.5998 4081,-18773.5998 4075,-18773.5998 4069,-18767.5998 4069,-18761.5998 4069,-18761.5998 4069,-18749.5998 4069,-18749.5998 4069,-18743.5998 4075,-18737.5998 4081,-18737.5998 4081,-18737.5998 4111,-18737.5998 4111,-18737.5998 4117,-18737.5998 4123,-18743.5998 4123,-18749.5998 4123,-18749.5998 4123,-18761.5998 4123,-18761.5998 4123,-18767.5998 4117,-18773.5998 4111,-18773.5998"/> +<text text-anchor="middle" x="4096" y="-18751.8998" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text> +</a> +</g> +</g> +<!-- golang.org/x/sys/unix->syscall --> +<g id="edge670" class="edge"> +<title>golang.org/x/sys/unix->syscall</title> +<path fill="none" stroke="#000000" d="M3848.6663,-13993.5967C3893.8052,-13991.6887 3950.7403,-13998.1205 3983,-14035.5998 4064.3523,-14130.1149 4025.6589,-18400.8422 4041,-18524.5998 4050.6054,-18602.0871 4076.1029,-18691.3592 4088.7196,-18732.5644"/> +<polygon fill="#000000" stroke="#000000" points="4087.1211,-18733.3198 4090.2655,-18737.583 4090.466,-18732.2894 4087.1211,-18733.3198"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->bytes --> +<g id="edge108" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->bytes</title> +<path fill="none" stroke="#000000" d="M2700.1195,-12366.6531C2710.3498,-12372.2861 2719.5882,-12379.4689 2727,-12388.5998 2804.1352,-12483.626 2737.862,-13375.6494 2785,-13488.5998 2887.2705,-13733.6568 2970.9604,-13776.2007 3187,-13930.5998 3481.318,-14140.943 3699.8647,-14325.7703 3983,-14100.5998 4072.6118,-14029.3337 4007.7317,-13959.1551 4041,-13849.5998 4054.3854,-13805.5205 4074.4473,-13755.59 4086.2816,-13727.3207"/> +<polygon fill="#000000" stroke="#000000" points="4087.926,-13727.9247 4088.25,-13722.6372 4084.6993,-13726.5686 4087.926,-13727.9247"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->compress/bzip2 --> +<g id="edge109" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->compress/bzip2</title> +<path fill="none" stroke="#000000" d="M2611.37,-12330.5144C2652.0507,-12310.0981 2721.8106,-12277.1842 2785,-12257.5998 2822.9236,-12245.8461 2866.9094,-12237.4972 2900.9064,-12232.1376"/> +<polygon fill="#000000" stroke="#000000" points="2901.1766,-12233.8667 2905.8479,-12231.3685 2900.6383,-12230.4084 2901.1766,-12233.8667"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->fmt --> +<g id="edge110" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->fmt</title> +<path fill="none" stroke="#000000" d="M2579.961,-12330.5447C2598.9352,-12230.6844 2690.7958,-11735.6989 2727,-11326.5998 2736.3681,-11220.7425 2722.8203,-9500.7812 2785,-9414.5998 2880.268,-9282.558 2978.2278,-9350.0715 3129,-9288.5998 3306.3873,-9216.2768 3382.159,-9242.5249 3522,-9111.5998 3562.0591,-9074.0948 3539.3379,-9039.4502 3580,-9002.5998 3723.5367,-8872.5185 3817.3427,-8937.0071 3983,-8836.5998 4017.8086,-8815.5018 4052.9172,-8784.02 4074.5576,-8763.1573"/> +<polygon fill="#000000" stroke="#000000" points="4075.8055,-8764.385 4078.1755,-8759.6472 4073.3683,-8761.8729 4075.8055,-8764.385"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->io --> +<g id="edge118" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->io</title> +<path fill="none" stroke="#000000" d="M2576.821,-12366.8678C2580.265,-12550.7864 2613.7056,-14070.0671 2785,-14483.5998 2892.6684,-14743.5288 2944.8844,-14829.2974 3187,-14972.5998 3339.8802,-15063.0859 3854.2075,-15168.9624 3983,-15046.5998 4074.305,-14959.8532 3976.1636,-14022.5712 4041,-13914.5998 4046.4191,-13905.5754 4055.3356,-13898.716 4064.4277,-13893.6533"/> +<polygon fill="#000000" stroke="#000000" points="4065.2768,-13895.184 4068.8906,-13891.3106 4063.65,-13892.085 4065.2768,-13895.184"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->io/ioutil --> +<g id="edge119" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2577.5304,-12366.8222C2588.5563,-12560.3527 2686.3024,-14239.9456 2785,-14730.5998 2883.9266,-15222.3926 3006.3306,-15319.1856 3129,-15805.5998 3163.3829,-15941.9365 3093.0522,-16011.9877 3187,-16116.5998 3308.8536,-16252.2854 3401.6156,-16196.6805 3580,-16234.5998 3758.896,-16272.6279 3977.655,-16291.7921 4061.2621,-16298.1403"/> +<polygon fill="#000000" stroke="#000000" points="4061.2169,-16299.8918 4066.3342,-16298.5223 4061.4798,-16296.4017 4061.2169,-16299.8918"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/pkg/errors --> +<g id="edge115" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2579.9724,-12330.5457C2599.0069,-12230.6907 2691.136,-11735.7289 2727,-11326.5998 2736.8183,-11214.5943 2713.1031,-9390.0434 2785,-9303.5998 2885.0016,-9183.3652 3003.09,-9322.3542 3129,-9229.5998 3171.2984,-9198.4398 3144.7878,-9157.8764 3187,-9126.5998 3309.5134,-9035.8251 3412.2031,-9158.4025 3522,-9052.5998 3595.7426,-8981.5398 3503.1665,-8897.3059 3580,-8829.5998 3614.2035,-8799.4595 3665.2311,-8793.1258 3707.3767,-8794.2428"/> +<polygon fill="#000000" stroke="#000000" points="3707.4144,-8795.9949 3712.4707,-8794.4148 3707.5327,-8792.4969 3707.4144,-8795.9949"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/sirupsen/logrus --> +<g id="edge116" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M2721.6794,-12342.8879C2833.1449,-12338.2311 2990.8607,-12331.0555 3129,-12322.5998 3174.47,-12319.8165 3225.0477,-12316.0639 3266.7616,-12312.7923"/> +<polygon fill="#000000" stroke="#000000" points="3266.9563,-12314.5325 3271.8037,-12312.3959 3266.6819,-12311.0433 3266.9563,-12314.5325"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression/internal --> +<g id="node78" class="node"> +<title>github.com/containers/image/v5/pkg/compression/internal</title> +<g id="a_node78"><a xlink:href="https://godoc.org/github.com/containers/image/v5/pkg/compression/internal" xlink:title="github.com/containers/image/v5/pkg/compression/internal" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3937,-14563.5998C3937,-14563.5998 3626,-14563.5998 3626,-14563.5998 3620,-14563.5998 3614,-14557.5998 3614,-14551.5998 3614,-14551.5998 3614,-14539.5998 3614,-14539.5998 3614,-14533.5998 3620,-14527.5998 3626,-14527.5998 3626,-14527.5998 3937,-14527.5998 3937,-14527.5998 3943,-14527.5998 3949,-14533.5998 3949,-14539.5998 3949,-14539.5998 3949,-14551.5998 3949,-14551.5998 3949,-14557.5998 3943,-14563.5998 3937,-14563.5998"/> +<text text-anchor="middle" x="3781.5" y="-14541.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/containers/image/v5/pkg/compression/internal</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/containers/image/v5/pkg/compression/internal --> +<g id="edge111" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/containers/image/v5/pkg/compression/internal</title> +<path fill="none" stroke="#000000" d="M2700.7019,-12366.6635C2710.7462,-12372.3037 2719.7841,-12379.4851 2727,-12388.5998 2868.5769,-12567.4336 2630.0196,-14273.2481 2785,-14440.5998 2798.8509,-14455.5564 3326.9195,-14504.9254 3608.5688,-14530.2782"/> +<polygon fill="#000000" stroke="#000000" points="3608.7994,-14532.056 3613.9362,-14530.7612 3609.1132,-14528.5701 3608.7994,-14532.056"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/containers/image/v5/pkg/compression/types --> +<g id="edge112" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/containers/image/v5/pkg/compression/types</title> +<path fill="none" stroke="#000000" d="M2700.6957,-12366.6685C2710.7412,-12372.3077 2719.7811,-12379.4874 2727,-12388.5998 2865.3946,-12563.295 2629.8448,-14233.605 2785,-14393.5998 2839.0053,-14449.2897 3040.1432,-14449.5577 3188.1375,-14441.3769"/> +<polygon fill="#000000" stroke="#000000" points="3188.4854,-14443.1102 3193.3794,-14441.0824 3188.289,-14439.6158 3188.4854,-14443.1102"/> +</g> +<!-- github.com/klauspost/compress/zstd --> +<g id="node80" class="node"> +<title>github.com/klauspost/compress/zstd</title> +<g id="a_node80"><a xlink:href="https://godoc.org/github.com/klauspost/compress/zstd" xlink:title="github.com/klauspost/compress/zstd" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3052.5,-11606.5998C3052.5,-11606.5998 2861.5,-11606.5998 2861.5,-11606.5998 2855.5,-11606.5998 2849.5,-11600.5998 2849.5,-11594.5998 2849.5,-11594.5998 2849.5,-11582.5998 2849.5,-11582.5998 2849.5,-11576.5998 2855.5,-11570.5998 2861.5,-11570.5998 2861.5,-11570.5998 3052.5,-11570.5998 3052.5,-11570.5998 3058.5,-11570.5998 3064.5,-11576.5998 3064.5,-11582.5998 3064.5,-11582.5998 3064.5,-11594.5998 3064.5,-11594.5998 3064.5,-11600.5998 3058.5,-11606.5998 3052.5,-11606.5998"/> +<text text-anchor="middle" x="2957" y="-11584.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/zstd</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/klauspost/compress/zstd --> +<g id="edge113" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/klauspost/compress/zstd</title> +<path fill="none" stroke="#000000" d="M2585.5304,-12330.5628C2636.5912,-12228.5753 2888.8602,-11724.7004 2945.6379,-11611.2941"/> +<polygon fill="#000000" stroke="#000000" points="2947.2116,-11612.0599 2947.8852,-11606.8054 2944.0819,-11610.4929 2947.2116,-11612.0599"/> +</g> +<!-- github.com/klauspost/pgzip --> +<g id="node81" class="node"> +<title>github.com/klauspost/pgzip</title> +<g id="a_node81"><a xlink:href="https://godoc.org/github.com/klauspost/pgzip" xlink:title="github.com/klauspost/pgzip" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3426,-11848.5998C3426,-11848.5998 3283,-11848.5998 3283,-11848.5998 3277,-11848.5998 3271,-11842.5998 3271,-11836.5998 3271,-11836.5998 3271,-11824.5998 3271,-11824.5998 3271,-11818.5998 3277,-11812.5998 3283,-11812.5998 3283,-11812.5998 3426,-11812.5998 3426,-11812.5998 3432,-11812.5998 3438,-11818.5998 3438,-11824.5998 3438,-11824.5998 3438,-11836.5998 3438,-11836.5998 3438,-11842.5998 3432,-11848.5998 3426,-11848.5998"/> +<text text-anchor="middle" x="3354.5" y="-11826.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/pgzip</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/klauspost/pgzip --> +<g id="edge114" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/klauspost/pgzip</title> +<path fill="none" stroke="#000000" d="M2598.0858,-12330.3693C2634.94,-12299.7302 2713.0091,-12236.9197 2785,-12192.5998 2930.3933,-12103.0911 2986.376,-12115.4583 3129,-12021.5998 3209.7532,-11968.4575 3293.794,-11890.0727 3332.5366,-11852.3923"/> +<polygon fill="#000000" stroke="#000000" points="3333.7852,-11853.619 3336.1428,-11848.8751 3331.3414,-11851.1134 3333.7852,-11853.619"/> +</g> +<!-- github.com/ulikunitz/xz --> +<g id="node82" class="node"> +<title>github.com/ulikunitz/xz</title> +<g id="a_node82"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz" xlink:title="github.com/ulikunitz/xz" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3018.5,-13238.5998C3018.5,-13238.5998 2895.5,-13238.5998 2895.5,-13238.5998 2889.5,-13238.5998 2883.5,-13232.5998 2883.5,-13226.5998 2883.5,-13226.5998 2883.5,-13214.5998 2883.5,-13214.5998 2883.5,-13208.5998 2889.5,-13202.5998 2895.5,-13202.5998 2895.5,-13202.5998 3018.5,-13202.5998 3018.5,-13202.5998 3024.5,-13202.5998 3030.5,-13208.5998 3030.5,-13214.5998 3030.5,-13214.5998 3030.5,-13226.5998 3030.5,-13226.5998 3030.5,-13232.5998 3024.5,-13238.5998 3018.5,-13238.5998"/> +<text text-anchor="middle" x="2957" y="-13216.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz</text> +</a> +</g> +</g> +<!-- github.com/containers/image/v5/pkg/compression->github.com/ulikunitz/xz --> +<g id="edge117" class="edge"> +<title>github.com/containers/image/v5/pkg/compression->github.com/ulikunitz/xz</title> +<path fill="none" stroke="#000000" d="M2694.4765,-12366.6534C2706.4933,-12372.2085 2717.6823,-12379.3742 2727,-12388.5998 2757.5922,-12418.8896 2918.6126,-13065.2953 2951.2333,-13197.2165"/> +<polygon fill="#000000" stroke="#000000" points="2949.6164,-13197.9687 2952.5151,-13202.4028 2953.0142,-13197.1289 2949.6164,-13197.9687"/> +</g> +<!-- github.com/containers/image/v5/pkg/strslice->encoding/json --> +<g id="edge136" class="edge"> +<title>github.com/containers/image/v5/pkg/strslice->encoding/json</title> +<path fill="none" stroke="#000000" d="M2971.1259,-17522.9023C3016.4642,-17581.3942 3157.0914,-17760.7883 3187,-17774.5998 3347.5926,-17848.7597 3845.645,-17886.0596 3983,-17774.5998 4064.8629,-17708.1704 4089.637,-17355.2528 4094.8474,-17260.8423"/> +<polygon fill="#000000" stroke="#000000" points="4096.5991,-17260.8559 4095.1222,-17255.7686 4093.1042,-17260.6666 4096.5991,-17260.8559"/> +</g> +<!-- github.com/containers/libtrust->bytes --> +<g id="edge172" class="edge"> +<title>github.com/containers/libtrust->bytes</title> +<path fill="none" stroke="#000000" d="M3785.0891,-16846.3275C3807.8692,-16729.4795 3932.9629,-16074.6783 3983,-15534.5998 4017.564,-15161.5313 3967.125,-14216.9107 4041,-13849.5998 4050.0331,-13804.6868 4071.3565,-13755.4192 4084.6415,-13727.4278"/> +<polygon fill="#000000" stroke="#000000" points="4086.2819,-13728.0539 4086.8625,-13722.7884 4083.125,-13726.5426 4086.2819,-13728.0539"/> +</g> +<!-- github.com/containers/libtrust->crypto --> +<g id="edge173" class="edge"> +<title>github.com/containers/libtrust->crypto</title> +<path fill="none" stroke="#000000" d="M3814.6144,-16846.3908C3858.7032,-16820.6107 3936.5095,-16769.5298 3983,-16706.5998 4025.718,-16648.7763 4013.1203,-16621.8653 4041,-16555.5998 4055.4142,-16521.3395 4073.6402,-16482.3246 4085.0625,-16458.331"/> +<polygon fill="#000000" stroke="#000000" points="4086.68,-16459.0047 4087.2531,-16453.7384 4083.5209,-16457.4979 4086.68,-16459.0047"/> +</g> +<!-- github.com/containers/libtrust->crypto/ecdsa --> +<g id="edge174" class="edge"> +<title>github.com/containers/libtrust->crypto/ecdsa</title> +<path fill="none" stroke="#000000" d="M3871.6964,-16864.8796C3909.6514,-16860.7126 3952.0842,-16849.8945 3983,-16824.5998 4034.8089,-16782.2108 3997.2222,-16736.2406 4041,-16685.5998 4044.8119,-16681.1903 4049.3644,-16677.2044 4054.1952,-16673.6532"/> +<polygon fill="#000000" stroke="#000000" points="4055.2167,-16675.0743 4058.3084,-16670.7726 4053.209,-16672.2074 4055.2167,-16675.0743"/> +</g> +<!-- github.com/containers/libtrust->crypto/elliptic --> +<g id="edge175" class="edge"> +<title>github.com/containers/libtrust->crypto/elliptic</title> +<path fill="none" stroke="#000000" d="M3871.5135,-16863.2271C3909.8865,-16867.1218 3952.6977,-16878.0292 3983,-16904.5998 4043.3409,-16957.5098 3990.1996,-17013.4724 4041,-17075.5998 4044.2517,-17079.5766 4048.1368,-17083.1644 4052.3178,-17086.3735"/> +<polygon fill="#000000" stroke="#000000" points="4051.5073,-17087.9468 4056.585,-17089.4566 4053.5571,-17085.1098 4051.5073,-17087.9468"/> +</g> +<!-- github.com/containers/libtrust->crypto/rand --> +<g id="edge176" class="edge"> +<title>github.com/containers/libtrust->crypto/rand</title> +<path fill="none" stroke="#000000" d="M3784.927,-16882.8288C3806.2209,-16996.696 3921.5295,-17622.096 3983,-18136.5998 4020.9274,-18454.0497 4001.0757,-18536.3949 4041,-18853.5998 4055.1304,-18965.8684 4080.78,-19098.7776 4091.3032,-19151.4457"/> +<polygon fill="#000000" stroke="#000000" points="4089.6345,-19152.0254 4092.3328,-19156.5841 4093.0662,-19151.3377 4089.6345,-19152.0254"/> +</g> +<!-- github.com/containers/libtrust->crypto/rsa --> +<g id="edge177" class="edge"> +<title>github.com/containers/libtrust->crypto/rsa</title> +<path fill="none" stroke="#000000" d="M3871.7378,-16866.0503C3909.1506,-16870.5964 3951.1545,-16881.2319 3983,-16904.5998 4026.2965,-16936.3704 4003.5288,-16972.1309 4041,-17010.5998 4045.484,-17015.2032 4050.7701,-17019.3669 4056.2623,-17023.0529"/> +<polygon fill="#000000" stroke="#000000" points="4055.3414,-17024.5413 4060.4925,-17025.7779 4057.2368,-17021.599 4055.3414,-17024.5413"/> +</g> +<!-- github.com/containers/libtrust->crypto/sha256 --> +<g id="edge178" class="edge"> +<title>github.com/containers/libtrust->crypto/sha256</title> +<path fill="none" stroke="#000000" d="M3871.6333,-16866.7233C3910.2607,-16863.0117 3953.2402,-16852.0405 3983,-16824.5998 4052.2972,-16760.7027 3983.6786,-16695.4276 4041,-16620.5998 4044.37,-16616.2006 4048.5082,-16612.2647 4052.9886,-16608.7782"/> +<polygon fill="#000000" stroke="#000000" points="4054.188,-16610.069 4057.1873,-16605.7023 4052.1196,-16607.2455 4054.188,-16610.069"/> +</g> +<!-- github.com/containers/libtrust->crypto/sha512 --> +<g id="edge179" class="edge"> +<title>github.com/containers/libtrust->crypto/sha512</title> +<path fill="none" stroke="#000000" d="M3871.6879,-16874.2561C3907.3634,-16880.1704 3948.099,-16889.6109 3983,-16904.5998 4012.0062,-16917.0571 4014.4423,-16928.5344 4041,-16945.5998 4046.7767,-16949.3118 4052.9905,-16953.1077 4059.0887,-16956.7279"/> +<polygon fill="#000000" stroke="#000000" points="4058.5242,-16958.4265 4063.7203,-16959.4578 4060.3014,-16955.4113 4058.5242,-16958.4265"/> +</g> +<!-- github.com/containers/libtrust->crypto/tls --> +<g id="edge180" class="edge"> +<title>github.com/containers/libtrust->crypto/tls</title> +<path fill="none" stroke="#000000" d="M3785.1114,-16882.808C3807.5191,-16996.5497 3928.3814,-17621.3236 3983,-18136.5998 4008.1239,-18373.6207 3943.8652,-18989.9419 4041,-19207.5998 4046.7328,-19220.4458 4056.9072,-19231.9443 4066.9591,-19241.065"/> +<polygon fill="#000000" stroke="#000000" points="4066.0343,-19242.582 4070.945,-19244.5689 4068.3452,-19239.9533 4066.0343,-19242.582"/> +</g> +<!-- github.com/containers/libtrust->crypto/x509 --> +<g id="edge181" class="edge"> +<title>github.com/containers/libtrust->crypto/x509</title> +<path fill="none" stroke="#000000" d="M3789.4609,-16882.9264C3819.6234,-16953.1094 3927.9478,-17212.0587 3983,-17435.5998 3992.3708,-17473.6503 4075.2746,-18067.6848 4092.7679,-18193.358"/> +<polygon fill="#000000" stroke="#000000" points="4091.0346,-18193.6003 4093.4572,-18198.3113 4094.5012,-18193.1178 4091.0346,-18193.6003"/> +</g> +<!-- github.com/containers/libtrust->crypto/x509/pkix --> +<g id="edge182" class="edge"> +<title>github.com/containers/libtrust->crypto/x509/pkix</title> +<path fill="none" stroke="#000000" d="M3871.6231,-16859.7283C3924.3374,-16856.8789 3989.4416,-16853.3597 4035.9196,-16850.8474"/> +<polygon fill="#000000" stroke="#000000" points="4036.0263,-16852.5943 4040.9245,-16850.5769 4035.8373,-16849.0994 4036.0263,-16852.5943"/> +</g> +<!-- github.com/containers/libtrust->encoding/base32 --> +<g id="edge183" class="edge"> +<title>github.com/containers/libtrust->encoding/base32</title> +<path fill="none" stroke="#000000" d="M3871.5046,-16849.9154C3906.5954,-16843.3639 3946.9679,-16834.8017 3983,-16824.5998 4004.626,-16818.4768 4027.9933,-16810.0999 4047.8353,-16802.4377"/> +<polygon fill="#000000" stroke="#000000" points="4048.5105,-16804.0529 4052.5367,-16800.6102 4047.2424,-16800.7907 4048.5105,-16804.0529"/> +</g> +<!-- github.com/containers/libtrust->encoding/base64 --> +<g id="edge184" class="edge"> +<title>github.com/containers/libtrust->encoding/base64</title> +<path fill="none" stroke="#000000" d="M3871.6231,-16878.3547C3924.8628,-16886.4803 3990.7406,-16896.5348 4037.3038,-16903.6414"/> +<polygon fill="#000000" stroke="#000000" points="4037.1083,-16905.3818 4042.3152,-16904.4063 4037.6365,-16901.9218 4037.1083,-16905.3818"/> +</g> +<!-- github.com/containers/libtrust->encoding/binary --> +<g id="edge185" class="edge"> +<title>github.com/containers/libtrust->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3785.1119,-16846.3295C3808.033,-16729.4946 3933.8456,-16074.7593 3983,-15534.5998 4058.0338,-14710.0499 4013.506,-14500.1001 4041,-13672.5998 4061.7862,-13046.9856 4089.9044,-12280.1632 4095.1417,-12137.8827"/> +<polygon fill="#000000" stroke="#000000" points="4096.8971,-12137.7666 4095.3323,-12132.7055 4093.3995,-12137.6377 4096.8971,-12137.7666"/> +</g> +<!-- github.com/containers/libtrust->encoding/json --> +<g id="edge186" class="edge"> +<title>github.com/containers/libtrust->encoding/json</title> +<path fill="none" stroke="#000000" d="M3871.738,-16861.212C3910.8588,-16864.5872 3954.2051,-16875.6431 3983,-16904.5998 4079.0649,-17001.2048 3962.1618,-17094.4894 4041,-17205.5998 4043.8767,-17209.6541 4047.4416,-17213.2719 4051.3637,-17216.4813"/> +<polygon fill="#000000" stroke="#000000" points="4050.3561,-17217.9137 4055.3927,-17219.5556 4052.4793,-17215.1312 4050.3561,-17217.9137"/> +</g> +<!-- github.com/containers/libtrust->encoding/pem --> +<g id="edge187" class="edge"> +<title>github.com/containers/libtrust->encoding/pem</title> +<path fill="none" stroke="#000000" d="M3789.9712,-16882.8105C3821.9775,-16952.5747 3936.0919,-17210.2091 3983,-17435.5998 4056.9002,-17790.6863 3987.7663,-17890.8327 4041,-18249.5998 4053.0927,-18331.0981 4077.8082,-18425.8719 4089.5411,-18468.614"/> +<polygon fill="#000000" stroke="#000000" points="4087.8898,-18469.2091 4090.9058,-18473.5641 4091.2639,-18468.2788 4087.8898,-18469.2091"/> +</g> +<!-- github.com/containers/libtrust->errors --> +<g id="edge188" class="edge"> +<title>github.com/containers/libtrust->errors</title> +<path fill="none" stroke="#000000" d="M3785.2815,-16846.3439C3809.2517,-16729.598 3940.4139,-16075.3168 3983,-15534.5998 4057.0592,-14594.2675 3940.7405,-7983.5005 4041,-7045.5998 4050.5963,-6955.8293 4077.1187,-6851.4736 4089.4894,-6805.8958"/> +<polygon fill="#000000" stroke="#000000" points="4091.2267,-6806.1766 4090.8548,-6800.8922 4087.8502,-6805.2551 4091.2267,-6806.1766"/> +</g> +<!-- github.com/containers/libtrust->fmt --> +<g id="edge189" class="edge"> +<title>github.com/containers/libtrust->fmt</title> +<path fill="none" stroke="#000000" d="M3785.2466,-16846.3412C3809.0013,-16729.578 3939.0645,-16075.2088 3983,-15534.5998 4071.2998,-14448.1058 4018.6838,-11719.4476 4041,-10629.5998 4056.674,-9864.1353 4089.5071,-8924.4748 4095.162,-8765.0867"/> +<polygon fill="#000000" stroke="#000000" points="4096.923,-8764.8065 4095.3516,-8759.7475 4093.4252,-8764.6822 4096.923,-8764.8065"/> +</g> +<!-- github.com/containers/libtrust->io --> +<g id="edge190" class="edge"> +<title>github.com/containers/libtrust->io</title> +<path fill="none" stroke="#000000" d="M3785.8471,-16846.5723C3810.7274,-16742.476 3935.6214,-16207.414 3983,-15762.5998 3993.8792,-15660.4604 3988.9954,-14003.1794 4041,-13914.5998 4046.3295,-13905.5221 4055.2225,-13898.6488 4064.3206,-13893.5897"/> +<polygon fill="#000000" stroke="#000000" points="4065.171,-13895.1199 4068.7887,-13891.2502 4063.5474,-13892.0192 4065.171,-13895.1199"/> +</g> +<!-- github.com/containers/libtrust->io/ioutil --> +<g id="edge191" class="edge"> +<title>github.com/containers/libtrust->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3818.2837,-16846.4671C3864.5696,-16821.6192 3942.852,-16772.4558 3983,-16706.5998 4054.3709,-16589.528 3995.3014,-16532.8718 4041,-16403.5998 4051.3015,-16374.4591 4068.8416,-16343.4939 4081.3439,-16323.2141"/> +<polygon fill="#000000" stroke="#000000" points="4082.9337,-16323.9716 4084.0888,-16318.8016 4079.9618,-16322.1228 4082.9337,-16323.9716"/> +</g> +<!-- github.com/containers/libtrust->sort --> +<g id="edge198" class="edge"> +<title>github.com/containers/libtrust->sort</title> +<path fill="none" stroke="#000000" d="M3785.2918,-16846.3447C3809.3255,-16729.6038 3940.8119,-16075.348 3983,-15534.5998 4076.5122,-14336.0026 3918.0602,-5910.5369 4041,-4714.5998 4050.6019,-4621.194 4077.4622,-4512.3577 4089.737,-4465.7252"/> +<polygon fill="#000000" stroke="#000000" points="4091.432,-4466.1605 4091.0192,-4460.8792 4088.0484,-4465.2652 4091.432,-4466.1605"/> +</g> +<!-- github.com/containers/libtrust->strings --> +<g id="edge199" class="edge"> +<title>github.com/containers/libtrust->strings</title> +<path fill="none" stroke="#000000" d="M3785.2758,-16846.3435C3809.2112,-16729.5948 3940.1957,-16075.2996 3983,-15534.5998 4049.5734,-14693.6507 4011.0011,-8785.6464 4041,-7942.5998 4052.9073,-7607.9726 4085.6689,-7201.5354 4094.0348,-7100.946"/> +<polygon fill="#000000" stroke="#000000" points="4095.7887,-7100.9706 4094.4601,-7095.8425 4092.3008,-7100.6798 4095.7887,-7100.9706"/> +</g> +<!-- github.com/containers/libtrust->sync --> +<g id="edge200" class="edge"> +<title>github.com/containers/libtrust->sync</title> +<path fill="none" stroke="#000000" d="M3785.2946,-16846.345C3809.3462,-16729.6054 3940.9235,-16075.3567 3983,-15534.5998 4084.0481,-14235.9537 3980.4511,-5112.7633 4041,-3811.5998 4051.9271,-3576.7811 4083.7079,-3293.5658 4093.2558,-3211.7512"/> +<polygon fill="#000000" stroke="#000000" points="4095.009,-3211.8257 4093.8521,-3206.6562 4091.5327,-3211.4188 4095.009,-3211.8257"/> +</g> +<!-- github.com/containers/libtrust->time --> +<g id="edge201" class="edge"> +<title>github.com/containers/libtrust->time</title> +<path fill="none" stroke="#000000" d="M3785.2209,-16846.339C3808.8162,-16729.5628 3938.0669,-16075.1268 3983,-15534.5998 4120.6458,-13878.7778 3917.7074,-13453.5524 4041,-11796.5998 4052.0413,-11648.2138 4081.106,-11471.5199 4091.8932,-11408.956"/> +<polygon fill="#000000" stroke="#000000" points="4093.6307,-11409.1785 4092.7589,-11403.9533 4090.1819,-11408.5817 4093.6307,-11409.1785"/> +</g> +<!-- github.com/containers/libtrust->unicode --> +<g id="edge202" class="edge"> +<title>github.com/containers/libtrust->unicode</title> +<path fill="none" stroke="#000000" d="M3785.293,-16846.3448C3809.3344,-16729.6045 3940.8597,-16075.3517 3983,-15534.5998 4031.2958,-14914.8593 3959.2798,-4961.8243 4041,-4345.5998 4050.1169,-4276.8521 4074.9391,-4198.4175 4087.8906,-4160.5073"/> +<polygon fill="#000000" stroke="#000000" points="4089.5896,-4160.948 4089.5608,-4155.6506 4086.2799,-4159.8097 4089.5896,-4160.948"/> +</g> +<!-- github.com/containers/libtrust->net/url --> +<g id="edge194" class="edge"> +<title>github.com/containers/libtrust->net/url</title> +<path fill="none" stroke="#000000" d="M3785.2987,-16846.3453C3809.3752,-16729.6077 3941.0794,-16075.3688 3983,-15534.5998 4096.9501,-14064.6625 3983.1768,-3738.8129 4041,-2265.5998 4052.1239,-1982.1862 4084.7108,-1639.0008 4093.6874,-1547.7768"/> +<polygon fill="#000000" stroke="#000000" points="4095.446,-1547.7753 4094.1954,-1542.6276 4091.9629,-1547.4316 4095.446,-1547.7753"/> +</g> +<!-- github.com/containers/libtrust->os --> +<g id="edge195" class="edge"> +<title>github.com/containers/libtrust->os</title> +<path fill="none" stroke="#000000" d="M3790.1713,-16882.7704C3822.9005,-16952.3894 3939.2848,-17209.5681 3983,-17435.5998 4011.5721,-17583.3336 3963.2057,-18659.7989 4041,-18788.5998 4046.3991,-18797.5389 4055.3103,-18804.2396 4064.4037,-18809.1361"/> +<polygon fill="#000000" stroke="#000000" points="4063.6165,-18810.699 4068.8678,-18811.3968 4065.1978,-18807.5766 4063.6165,-18810.699"/> +</g> +<!-- github.com/containers/libtrust->path --> +<g id="edge196" class="edge"> +<title>github.com/containers/libtrust->path</title> +<path fill="none" stroke="#000000" d="M3785.2965,-16846.3451C3809.3594,-16729.6064 3940.9943,-16075.3622 3983,-15534.5998 4089.5144,-14163.3821 3947.0252,-4528.7339 4041,-3156.5998 4051.264,-3006.735 4080.9365,-2828.2925 4091.8814,-2765.6787"/> +<polygon fill="#000000" stroke="#000000" points="4093.6187,-2765.9031 4092.7592,-2760.6759 4090.1714,-2765.2982 4093.6187,-2765.9031"/> +</g> +<!-- github.com/containers/libtrust->path/filepath --> +<g id="edge197" class="edge"> +<title>github.com/containers/libtrust->path/filepath</title> +<path fill="none" stroke="#000000" d="M3785.1399,-16882.805C3807.7192,-16996.5287 3929.438,-17621.2127 3983,-18136.5998 4009.6566,-18393.0962 3961.8035,-19050.184 4041,-19295.5998 4048.5639,-19319.0391 4064.3258,-19342.2853 4077.0449,-19358.5273"/> +<polygon fill="#000000" stroke="#000000" points="4075.724,-19359.6779 4080.2061,-19362.5015 4078.4631,-19357.4991 4075.724,-19359.6779"/> +</g> +<!-- github.com/containers/libtrust->net --> +<g id="edge193" class="edge"> +<title>github.com/containers/libtrust->net</title> +<path fill="none" stroke="#000000" d="M3871.5857,-16861.935C3910.4377,-16865.4994 3953.6103,-16876.5044 3983,-16904.5998 4061.0745,-16979.2359 3976.2874,-17054.1218 4041,-17140.5998 4046.9755,-17148.5851 4055.5767,-17154.918 4064.2084,-17159.7709"/> +<polygon fill="#000000" stroke="#000000" points="4063.7545,-17161.5117 4068.9905,-17162.3158 4065.3987,-17158.4219 4063.7545,-17161.5117"/> +</g> +<!-- math/big --> +<g id="node88" class="node"> +<title>math/big</title> +<g id="a_node88"><a xlink:href="https://godoc.org/math/big" xlink:title="math/big" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4116.5,-16735.5998C4116.5,-16735.5998 4075.5,-16735.5998 4075.5,-16735.5998 4069.5,-16735.5998 4063.5,-16729.5998 4063.5,-16723.5998 4063.5,-16723.5998 4063.5,-16711.5998 4063.5,-16711.5998 4063.5,-16705.5998 4069.5,-16699.5998 4075.5,-16699.5998 4075.5,-16699.5998 4116.5,-16699.5998 4116.5,-16699.5998 4122.5,-16699.5998 4128.5,-16705.5998 4128.5,-16711.5998 4128.5,-16711.5998 4128.5,-16723.5998 4128.5,-16723.5998 4128.5,-16729.5998 4122.5,-16735.5998 4116.5,-16735.5998"/> +<text text-anchor="middle" x="4096" y="-16713.8998" font-family="Times,serif" font-size="14.00" fill="#000000">math/big</text> +</a> +</g> +</g> +<!-- github.com/containers/libtrust->math/big --> +<g id="edge192" class="edge"> +<title>github.com/containers/libtrust->math/big</title> +<path fill="none" stroke="#000000" d="M3871.5021,-16860.3466C3908.2416,-16855.272 3949.8022,-16844.9989 3983,-16824.5998 4018.6029,-16802.7228 4009.673,-16778.2547 4041,-16750.5998 4046.4262,-16745.8096 4052.6427,-16741.3245 4058.8983,-16737.2984"/> +<polygon fill="#000000" stroke="#000000" points="4059.9186,-16738.7242 4063.2262,-16734.5862 4058.06,-16735.7585 4059.9186,-16738.7242"/> +</g> +<!-- github.com/docker/docker/api/types/versions->strconv --> +<g id="edge302" class="edge"> +<title>github.com/docker/docker/api/types/versions->strconv</title> +<path fill="none" stroke="#000000" d="M3802.5551,-5811.4643C3842.2215,-5776.407 3928.89,-5695.4635 3983,-5612.5998 4035.8773,-5531.6239 4073.9889,-5421.7758 4088.9765,-5374.6356"/> +<polygon fill="#000000" stroke="#000000" points="4090.6865,-5375.0317 4090.5223,-5369.7369 4087.3487,-5373.9785 4090.6865,-5375.0317"/> +</g> +<!-- github.com/docker/docker/api/types/versions->strings --> +<g id="edge303" class="edge"> +<title>github.com/docker/docker/api/types/versions->strings</title> +<path fill="none" stroke="#000000" d="M3807.3322,-5847.8478C3852.3283,-5881.3125 3944.0026,-5957.3216 3983,-6046.5998 4051.5987,-6203.6455 4016.8993,-6645.9287 4041,-6815.5998 4053.6503,-6904.659 4078.6089,-7008.8074 4090.0348,-7054.3264"/> +<polygon fill="#000000" stroke="#000000" points="4088.3753,-7054.9029 4091.294,-7059.3237 4091.7692,-7054.0477 4088.3753,-7054.9029"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go->fmt --> +<g id="edge491" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go->fmt</title> +<path fill="none" stroke="#000000" d="M3922.0726,-9518.5508C3944.9759,-9511.7599 3966.5478,-9500.8067 3983,-9483.5998 4034.2426,-9430.0067 4083.6945,-8885.1314 4094.0305,-8764.9651"/> +<polygon fill="#000000" stroke="#000000" points="4095.782,-8765.0216 4094.4654,-8759.8904 4092.2947,-8764.7227 4095.782,-8765.0216"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression/internal->io --> +<g id="edge120" class="edge"> +<title>github.com/containers/image/v5/pkg/compression/internal->io</title> +<path fill="none" stroke="#000000" d="M3949.3171,-14530.7998C3961.9131,-14524.4138 3973.4193,-14516.1701 3983,-14505.5998 4071.6236,-14407.8224 3970.4445,-14026.1188 4041,-13914.5998 4046.4755,-13905.9453 4055.1153,-13899.25 4063.9391,-13894.2233"/> +<polygon fill="#000000" stroke="#000000" points="4065.2582,-13895.5023 4068.8412,-13891.6004 4063.607,-13892.4163 4065.2582,-13895.5023"/> +</g> +<!-- github.com/containers/image/v5/pkg/compression/types->github.com/containers/image/v5/pkg/compression/internal --> +<g id="edge121" class="edge"> +<title>github.com/containers/image/v5/pkg/compression/types->github.com/containers/image/v5/pkg/compression/internal</title> +<path fill="none" stroke="#000000" d="M3455.0915,-14445.6585C3477.5914,-14451.3251 3500.9986,-14458.5415 3522,-14467.5998 3550.2977,-14479.8051 3551.4729,-14493.9408 3580,-14505.5998 3601.1901,-14514.2602 3624.4212,-14521.0924 3647.3376,-14526.4686"/> +<polygon fill="#000000" stroke="#000000" points="3646.9569,-14528.1767 3652.2221,-14527.5939 3647.7426,-14524.766 3646.9569,-14528.1767"/> +</g> +<!-- github.com/klauspost/compress/zstd->bytes --> +<g id="edge447" class="edge"> +<title>github.com/klauspost/compress/zstd->bytes</title> +<path fill="none" stroke="#000000" d="M2963.9657,-11607.0273C2991.1564,-11679.9411 3090.5388,-11955.8374 3129,-12192.5998 3152.1483,-12335.098 3094.8699,-13379.453 3187,-13490.5998 3245.4114,-13561.0681 3908.0938,-13673.734 4063.5184,-13699.3173"/> +<polygon fill="#000000" stroke="#000000" points="4063.6139,-13701.1064 4068.8316,-13700.1905 4064.1815,-13697.6527 4063.6139,-13701.1064"/> +</g> +<!-- github.com/klauspost/compress/zstd->crypto/rand --> +<g id="edge448" class="edge"> +<title>github.com/klauspost/compress/zstd->crypto/rand</title> +<path fill="none" stroke="#000000" d="M2964.2179,-11606.9886C2992.3519,-11679.7578 3094.7893,-11955.186 3129,-12192.5998 3155.1131,-12373.8185 3085.64,-18631.1261 3187,-18783.5998 3291.2419,-18940.4088 3883.4849,-19115.3678 4051.5414,-19162.4126"/> +<polygon fill="#000000" stroke="#000000" points="4051.1023,-19164.1069 4056.3889,-19163.767 4052.0442,-19160.736 4051.1023,-19164.1069"/> +</g> +<!-- github.com/klauspost/compress/zstd->encoding/binary --> +<g id="edge449" class="edge"> +<title>github.com/klauspost/compress/zstd->encoding/binary</title> +<path fill="none" stroke="#000000" d="M2963.9367,-11607.032C2991.019,-11679.9635 3090.0502,-11955.9173 3129,-12192.5998 3150.4599,-12323.0031 3091.2994,-13289.4575 3187,-13380.5998 3443.1854,-13624.583 3721.958,-13619.3796 3983,-13380.5998 4078.436,-13293.3028 4093.8439,-12304.04 4095.7615,-12138.1384"/> +<polygon fill="#000000" stroke="#000000" points="4097.5125,-12138.0469 4095.8193,-12133.0274 4094.0128,-12138.0072 4097.5125,-12138.0469"/> +</g> +<!-- github.com/klauspost/compress/zstd->encoding/hex --> +<g id="edge450" class="edge"> +<title>github.com/klauspost/compress/zstd->encoding/hex</title> +<path fill="none" stroke="#000000" d="M2975.2678,-11570.4623C3012.4956,-11532.1096 3096.3739,-11438.2543 3129,-11340.5998 3182.0639,-11181.7722 3079.6415,-9963.1149 3187,-9834.5998 3284.7551,-9717.5805 3418.9187,-9872.9556 3522,-9760.5998 3655.0491,-9615.5799 3445.4039,-9465.1851 3580,-9321.5998 3618.2827,-9280.7603 3685.3637,-9274.9623 3731.5414,-9276.5892"/> +<polygon fill="#000000" stroke="#000000" points="3731.7155,-9278.348 3736.7844,-9276.8088 3731.862,-9274.851 3731.7155,-9278.348"/> +</g> +<!-- github.com/klauspost/compress/zstd->errors --> +<g id="edge451" class="edge"> +<title>github.com/klauspost/compress/zstd->errors</title> +<path fill="none" stroke="#000000" d="M2963.5039,-11570.4911C2990.435,-11494.4684 3093.4599,-11193.1272 3129,-10935.5998 3155.3354,-10744.7704 3112.452,-7646.2287 3187,-7468.5998 3269.0941,-7272.9904 3419.7416,-7314.4645 3522,-7128.5998 3576.9823,-7028.6642 3497.5902,-6957.4592 3580,-6878.5998 3584.4605,-6874.3315 3951.4688,-6808.4365 4063.736,-6788.3607"/> +<polygon fill="#000000" stroke="#000000" points="4064.2731,-6790.0425 4068.887,-6787.4397 4063.657,-6786.5971 4064.2731,-6790.0425"/> +</g> +<!-- github.com/klauspost/compress/zstd->fmt --> +<g id="edge452" class="edge"> +<title>github.com/klauspost/compress/zstd->fmt</title> +<path fill="none" stroke="#000000" d="M2975.2291,-11570.4494C3012.3846,-11532.0724 3096.1351,-11438.1742 3129,-11340.5998 3176.428,-11199.7883 3090.3289,-10117.4359 3187,-10004.5998 3285.7877,-9889.2932 3415.9825,-10046.2962 3522,-9937.5998 3626.8938,-9830.0555 3470.949,-9707.9263 3580,-9604.5998 3710.9363,-9480.5369 3854.2341,-9682.9139 3983,-9556.5998 4041.3992,-9499.3127 4085.7645,-8891.972 4094.4509,-8764.8492"/> +<polygon fill="#000000" stroke="#000000" points="4096.198,-8764.9497 4094.7915,-8759.8425 4092.7061,-8764.7121 4096.198,-8764.9497"/> +</g> +<!-- github.com/klauspost/compress/zstd->io --> +<g id="edge458" class="edge"> +<title>github.com/klauspost/compress/zstd->io</title> +<path fill="none" stroke="#000000" d="M2964.0715,-11607.0105C2991.658,-11679.8615 3092.3223,-11955.5546 3129,-12192.5998 3145.6489,-12300.2004 3120.7301,-14065.209 3187,-14151.5998 3297.9987,-14296.3 3399.566,-14243.0962 3580,-14269.5998 3757.2096,-14295.6298 3846.52,-14385.5912 3983,-14269.5998 4104.8183,-14166.0691 3951.2377,-14046.8914 4041,-13914.5998 4046.6974,-13906.203 4055.3075,-13899.6028 4064.0349,-13894.584"/> +<polygon fill="#000000" stroke="#000000" points="4065.3171,-13895.8794 4068.8777,-13891.9571 4063.6482,-13892.8028 4065.3171,-13895.8794"/> +</g> +<!-- github.com/klauspost/compress/zstd->io/ioutil --> +<g id="edge459" class="edge"> +<title>github.com/klauspost/compress/zstd->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2964.1593,-11606.9972C2992.0741,-11679.7985 3093.8016,-11955.3304 3129,-12192.5998 3156.616,-12378.7568 3094.0299,-15415.9733 3187,-15579.5998 3289.7878,-15760.5052 3842.979,-15918.6966 3983,-16072.5998 4040.3968,-16135.6872 4075.4893,-16233.2907 4089.2755,-16277.471"/> +<polygon fill="#000000" stroke="#000000" points="4087.6297,-16278.073 4090.7746,-16282.336 4090.9746,-16277.0423 4087.6297,-16278.073"/> +</g> +<!-- github.com/klauspost/compress/zstd->math --> +<g id="edge461" class="edge"> +<title>github.com/klauspost/compress/zstd->math</title> +<path fill="none" stroke="#000000" d="M2963.5642,-11570.4993C2990.7355,-11494.5092 3094.583,-11193.2797 3129,-10935.5998 3142.3166,-10835.8986 3135.1927,-3779.8185 3187,-3693.5998 3375.6247,-3379.6871 3688.0554,-3263.5096 3983,-3480.5998 4039.713,-3522.3427 4080.3899,-3746.6903 4092.3624,-3820.2359"/> +<polygon fill="#000000" stroke="#000000" points="4090.6499,-3820.6089 4093.1746,-3825.266 4094.1051,-3820.0509 4090.6499,-3820.6089"/> +</g> +<!-- github.com/klauspost/compress/zstd->strconv --> +<g id="edge465" class="edge"> +<title>github.com/klauspost/compress/zstd->strconv</title> +<path fill="none" stroke="#000000" d="M2963.5144,-11570.4925C2990.4874,-11494.4756 3093.6559,-11193.1542 3129,-10935.5998 3157.8108,-10725.6541 3101.7328,-7315.6018 3187,-7121.5998 3269.4782,-6933.9434 3431.6884,-6988.6145 3522,-6804.5998 3636.3339,-6571.6385 3462.4948,-6454.9777 3580,-6223.5998 3688.8833,-6009.1992 3861.6499,-6070.1989 3983,-5862.5998 4033.2946,-5776.5586 4080.3726,-5462.7219 4092.8122,-5374.6955"/> +<polygon fill="#000000" stroke="#000000" points="4094.5502,-5374.9025 4093.5141,-5369.7074 4091.0844,-5374.4147 4094.5502,-5374.9025"/> +</g> +<!-- github.com/klauspost/compress/zstd->strings --> +<g id="edge466" class="edge"> +<title>github.com/klauspost/compress/zstd->strings</title> +<path fill="none" stroke="#000000" d="M2975.2826,-11570.4673C3012.5381,-11532.1238 3096.4652,-11438.2848 3129,-11340.5998 3240.2338,-11006.6226 3067.1105,-10088.5684 3187,-9757.5998 3284.2205,-9489.2116 3385.1196,-9458.18 3580,-9249.5998 3743.5599,-9074.5419 3878.1991,-9107.0387 3983,-8891.5998 4029.2118,-8796.6023 4034.426,-8048.0363 4041,-7942.5998 4061.8368,-7608.4098 4088.0046,-7201.6498 4094.493,-7100.9684"/> +<polygon fill="#000000" stroke="#000000" points="4096.2469,-7100.9624 4094.8222,-7095.8602 4092.7542,-7100.7373 4096.2469,-7100.9624"/> +</g> +<!-- github.com/klauspost/compress/zstd->sync --> +<g id="edge467" class="edge"> +<title>github.com/klauspost/compress/zstd->sync</title> +<path fill="none" stroke="#000000" d="M2963.5664,-11570.4996C2990.7466,-11494.5107 3094.6247,-11193.2853 3129,-10935.5998 3156.7212,-10727.7956 3090.3995,-3574.6627 3187,-3388.5998 3236.47,-3293.3153 3477.7683,-3140.3886 3580,-3107.5998 3750.5535,-3052.8981 3812.4791,-3052.7966 3983,-3107.5998 4019.1279,-3119.2109 4053.4993,-3147.1094 4074.6462,-3166.9114"/> +<polygon fill="#000000" stroke="#000000" points="4073.6064,-3168.3373 4078.4383,-3170.5089 4076.0153,-3165.7981 4073.6064,-3168.3373"/> +</g> +<!-- github.com/klauspost/compress/zstd->math/bits --> +<g id="edge462" class="edge"> +<title>github.com/klauspost/compress/zstd->math/bits</title> +<path fill="none" stroke="#000000" d="M3064.777,-11578.1643C3088.1907,-11571.5151 3111.234,-11561.0256 3129,-11544.5998 3187.6172,-11490.4043 3125.5398,-11425.5486 3187,-11374.5998 3302.7957,-11278.6083 3404.6102,-11420.6352 3522,-11326.5998 3578.7572,-11281.1343 3526.6241,-11222.9912 3580,-11173.5998 3716.982,-11046.8435 3851.3977,-11187.9329 3983,-11055.5998 4036.7802,-11001.521 4079.7233,-10761.7661 4092.2715,-10685.1934"/> +<polygon fill="#000000" stroke="#000000" points="4094.0461,-10685.1832 4093.1222,-10679.967 4090.5916,-10684.6209 4094.0461,-10685.1832"/> +</g> +<!-- github.com/klauspost/compress/zstd->runtime --> +<g id="edge463" class="edge"> +<title>github.com/klauspost/compress/zstd->runtime</title> +<path fill="none" stroke="#000000" d="M2964.1905,-11606.9926C2992.2223,-11679.7767 3094.3285,-11955.2529 3129,-12192.5998 3275.9021,-13198.2307 3036.119,-15760.5582 3187,-16765.5998 3265.5271,-17288.6811 3396.2386,-17393.825 3522,-17907.5998 3553.6661,-18036.9657 3477.113,-18117.0272 3580,-18201.5998 3649.1827,-18258.4676 3911.8738,-18256.0172 3983,-18201.5998 4047.4122,-18152.3192 4083.6361,-17888.7355 4093.3319,-17808.0725"/> +<polygon fill="#000000" stroke="#000000" points="4095.079,-17808.1999 4093.9322,-17803.0281 4091.6036,-17807.7862 4095.079,-17808.1999"/> +</g> +<!-- github.com/klauspost/compress/zstd->log --> +<g id="edge460" class="edge"> +<title>github.com/klauspost/compress/zstd->log</title> +<path fill="none" stroke="#000000" d="M3056.1569,-11570.5525C3169.3009,-11549.5059 3359.6772,-11512.7108 3522,-11474.5998 3730.0471,-11425.7535 3977.1862,-11355.1386 4063.9711,-11329.9553"/> +<polygon fill="#000000" stroke="#000000" points="4064.6515,-11331.58 4068.965,-11328.5049 4063.6753,-11328.2189 4064.6515,-11331.58"/> +</g> +<!-- github.com/klauspost/compress/huff0 --> +<g id="node115" class="node"> +<title>github.com/klauspost/compress/huff0</title> +<g id="a_node115"><a xlink:href="https://godoc.org/github.com/klauspost/compress/huff0" xlink:title="github.com/klauspost/compress/huff0" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3453.5,-10054.5998C3453.5,-10054.5998 3255.5,-10054.5998 3255.5,-10054.5998 3249.5,-10054.5998 3243.5,-10048.5998 3243.5,-10042.5998 3243.5,-10042.5998 3243.5,-10030.5998 3243.5,-10030.5998 3243.5,-10024.5998 3249.5,-10018.5998 3255.5,-10018.5998 3255.5,-10018.5998 3453.5,-10018.5998 3453.5,-10018.5998 3459.5,-10018.5998 3465.5,-10024.5998 3465.5,-10030.5998 3465.5,-10030.5998 3465.5,-10042.5998 3465.5,-10042.5998 3465.5,-10048.5998 3459.5,-10054.5998 3453.5,-10054.5998"/> +<text text-anchor="middle" x="3354.5" y="-10032.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/huff0</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/zstd->github.com/klauspost/compress/huff0 --> +<g id="edge453" class="edge"> +<title>github.com/klauspost/compress/zstd->github.com/klauspost/compress/huff0</title> +<path fill="none" stroke="#000000" d="M2975.1941,-11570.4375C3012.2842,-11532.0384 3095.9189,-11438.1011 3129,-11340.5998 3172.3177,-11212.9275 3115.594,-10242.9581 3187,-10128.5998 3209.0303,-10093.3179 3249.6553,-10070.5283 3285.3786,-10056.4572"/> +<polygon fill="#000000" stroke="#000000" points="3286.1228,-10058.0459 3290.1585,-10054.6143 3284.8637,-10054.7802 3286.1228,-10058.0459"/> +</g> +<!-- github.com/klauspost/compress/snappy --> +<g id="node116" class="node"> +<title>github.com/klauspost/compress/snappy</title> +<g id="a_node116"><a xlink:href="https://godoc.org/github.com/klauspost/compress/snappy" xlink:title="github.com/klauspost/compress/snappy" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3458,-12173.5998C3458,-12173.5998 3251,-12173.5998 3251,-12173.5998 3245,-12173.5998 3239,-12167.5998 3239,-12161.5998 3239,-12161.5998 3239,-12149.5998 3239,-12149.5998 3239,-12143.5998 3245,-12137.5998 3251,-12137.5998 3251,-12137.5998 3458,-12137.5998 3458,-12137.5998 3464,-12137.5998 3470,-12143.5998 3470,-12149.5998 3470,-12149.5998 3470,-12161.5998 3470,-12161.5998 3470,-12167.5998 3464,-12173.5998 3458,-12173.5998"/> +<text text-anchor="middle" x="3354.5" y="-12151.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/snappy</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/zstd->github.com/klauspost/compress/snappy --> +<g id="edge454" class="edge"> +<title>github.com/klauspost/compress/zstd->github.com/klauspost/compress/snappy</title> +<path fill="none" stroke="#000000" d="M2986.5079,-11606.6914C3025.8753,-11632.517 3094.6006,-11683.8424 3129,-11746.5998 3200.1568,-11876.4165 3097.6805,-11956.5418 3187,-12074.5998 3209.2907,-12104.0626 3245.0981,-12123.3671 3277.8949,-12135.7094"/> +<polygon fill="#000000" stroke="#000000" points="3277.4132,-12137.3967 3282.7099,-12137.4802 3278.6213,-12134.1119 3277.4132,-12137.3967"/> +</g> +<!-- hash/crc32 --> +<g id="node117" class="node"> +<title>hash/crc32</title> +<g id="a_node117"><a xlink:href="https://godoc.org/hash/crc32" xlink:title="hash/crc32" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3807,-12173.5998C3807,-12173.5998 3756,-12173.5998 3756,-12173.5998 3750,-12173.5998 3744,-12167.5998 3744,-12161.5998 3744,-12161.5998 3744,-12149.5998 3744,-12149.5998 3744,-12143.5998 3750,-12137.5998 3756,-12137.5998 3756,-12137.5998 3807,-12137.5998 3807,-12137.5998 3813,-12137.5998 3819,-12143.5998 3819,-12149.5998 3819,-12149.5998 3819,-12161.5998 3819,-12161.5998 3819,-12167.5998 3813,-12173.5998 3807,-12173.5998"/> +<text text-anchor="middle" x="3781.5" y="-12151.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash/crc32</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/zstd->hash/crc32 --> +<g id="edge457" class="edge"> +<title>github.com/klauspost/compress/zstd->hash/crc32</title> +<path fill="none" stroke="#000000" d="M2985.1798,-11606.6339C3023.6567,-11632.7599 3092.0597,-11684.8307 3129,-11746.5998 3182.4455,-11835.9678 3117.0087,-11896.5006 3187,-11973.5998 3263.3011,-12057.6497 3615.5432,-12126.4461 3738.7819,-12148.3156"/> +<polygon fill="#000000" stroke="#000000" points="3738.545,-12150.0508 3743.7733,-12149.1979 3739.1543,-12146.6043 3738.545,-12150.0508"/> +</g> +<!-- github.com/klauspost/compress/zstd/internal/xxhash --> +<g id="node118" class="node"> +<title>github.com/klauspost/compress/zstd/internal/xxhash</title> +<g id="a_node118"><a xlink:href="https://godoc.org/github.com/klauspost/compress/zstd/internal/xxhash" xlink:title="github.com/klauspost/compress/zstd/internal/xxhash" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3920.5,-11223.5998C3920.5,-11223.5998 3642.5,-11223.5998 3642.5,-11223.5998 3636.5,-11223.5998 3630.5,-11217.5998 3630.5,-11211.5998 3630.5,-11211.5998 3630.5,-11199.5998 3630.5,-11199.5998 3630.5,-11193.5998 3636.5,-11187.5998 3642.5,-11187.5998 3642.5,-11187.5998 3920.5,-11187.5998 3920.5,-11187.5998 3926.5,-11187.5998 3932.5,-11193.5998 3932.5,-11199.5998 3932.5,-11199.5998 3932.5,-11211.5998 3932.5,-11211.5998 3932.5,-11217.5998 3926.5,-11223.5998 3920.5,-11223.5998"/> +<text text-anchor="middle" x="3781.5" y="-11201.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/zstd/internal/xxhash</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/zstd->github.com/klauspost/compress/zstd/internal/xxhash --> +<g id="edge455" class="edge"> +<title>github.com/klauspost/compress/zstd->github.com/klauspost/compress/zstd/internal/xxhash</title> +<path fill="none" stroke="#000000" d="M3048.4615,-11570.5792C3074.7353,-11564.5305 3103.2404,-11557.1206 3129,-11548.5998 3309.8477,-11488.7789 3354.6396,-11468.5665 3522,-11377.5998 3610.0453,-11329.7439 3706.6717,-11260.9889 3753.2685,-11226.7079"/> +<polygon fill="#000000" stroke="#000000" points="3754.3629,-11228.0752 3757.3492,-11223.6997 3752.2861,-11225.2579 3754.3629,-11228.0752"/> +</g> +<!-- github.com/klauspost/compress/zstd->hash --> +<g id="edge456" class="edge"> +<title>github.com/klauspost/compress/zstd->hash</title> +<path fill="none" stroke="#000000" d="M3064.8418,-11585.4779C3088.3908,-11589.7326 3111.478,-11598.4073 3129,-11614.5998 3212.4515,-11691.7196 3098.0036,-11792.9511 3187,-11863.5998 3459.639,-12080.0312 3936.6535,-11897.8219 4063.9967,-11842.9857"/> +<polygon fill="#000000" stroke="#000000" points="4064.8412,-11844.5271 4068.7344,-11840.9347 4063.4507,-11841.3152 4064.8412,-11844.5271"/> +</g> +<!-- runtime/debug --> +<g id="node120" class="node"> +<title>runtime/debug</title> +<g id="a_node120"><a xlink:href="https://godoc.org/runtime/debug" xlink:title="runtime/debug" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3390.5,-11311.5998C3390.5,-11311.5998 3318.5,-11311.5998 3318.5,-11311.5998 3312.5,-11311.5998 3306.5,-11305.5998 3306.5,-11299.5998 3306.5,-11299.5998 3306.5,-11287.5998 3306.5,-11287.5998 3306.5,-11281.5998 3312.5,-11275.5998 3318.5,-11275.5998 3318.5,-11275.5998 3390.5,-11275.5998 3390.5,-11275.5998 3396.5,-11275.5998 3402.5,-11281.5998 3402.5,-11287.5998 3402.5,-11287.5998 3402.5,-11299.5998 3402.5,-11299.5998 3402.5,-11305.5998 3396.5,-11311.5998 3390.5,-11311.5998"/> +<text text-anchor="middle" x="3354.5" y="-11289.8998" font-family="Times,serif" font-size="14.00" fill="#000000">runtime/debug</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/zstd->runtime/debug --> +<g id="edge464" class="edge"> +<title>github.com/klauspost/compress/zstd->runtime/debug</title> +<path fill="none" stroke="#000000" d="M3064.5366,-11581.9197C3088.2356,-11575.8025 3111.4667,-11565.53 3129,-11548.5998 3200.1973,-11479.8517 3116.0065,-11402.5583 3187,-11333.5998 3217.0477,-11304.4134 3264.378,-11295.193 3301.0727,-11292.8262"/> +<polygon fill="#000000" stroke="#000000" points="3301.5128,-11294.5542 3306.4062,-11292.5252 3301.3155,-11291.0598 3301.5128,-11294.5542"/> +</g> +<!-- github.com/klauspost/pgzip->bufio --> +<g id="edge471" class="edge"> +<title>github.com/klauspost/pgzip->bufio</title> +<path fill="none" stroke="#000000" d="M3365.759,-11848.9197C3444.3587,-11977.1737 3910.0224,-12743.0252 3983,-13003.5998 4037.7072,-13198.938 3934.3286,-13742.0568 4041,-13914.5998 4046.4915,-13923.4824 4055.4269,-13930.1683 4064.514,-13935.0687"/> +<polygon fill="#000000" stroke="#000000" points="4063.7222,-13936.6293 4068.9727,-13937.3327 4065.3068,-13933.5086 4063.7222,-13936.6293"/> +</g> +<!-- github.com/klauspost/pgzip->bytes --> +<g id="edge472" class="edge"> +<title>github.com/klauspost/pgzip->bytes</title> +<path fill="none" stroke="#000000" d="M3438.1999,-11842.7167C3593.1351,-11866.9979 3915.0715,-11926.7425 3983,-12012.5998 4037.2921,-12081.2216 4088.2131,-13482.0157 4095.1888,-13681.134"/> +<polygon fill="#000000" stroke="#000000" points="4093.4495,-13681.4738 4095.3732,-13686.4096 4096.9474,-13681.3515 4093.4495,-13681.4738"/> +</g> +<!-- github.com/klauspost/pgzip->errors --> +<g id="edge473" class="edge"> +<title>github.com/klauspost/pgzip->errors</title> +<path fill="none" stroke="#000000" d="M3362.7075,-11812.338C3391.2911,-11747.6968 3486.6441,-11523.0692 3522,-11326.5998 3538.1961,-11236.5998 3524.4665,-9754.252 3580,-9681.5998 3693.8836,-9532.6107 3870.0236,-9706.278 3983,-9556.5998 4067.0638,-9445.2269 4025.0406,-7184.2214 4041,-7045.5998 4051.3259,-6955.9103 4077.4747,-6851.5131 4089.6197,-6805.9102"/> +<polygon fill="#000000" stroke="#000000" points="4091.3573,-6806.1864 4090.9598,-6800.9039 4087.9763,-6805.2813 4091.3573,-6806.1864"/> +</g> +<!-- github.com/klauspost/pgzip->fmt --> +<g id="edge474" class="edge"> +<title>github.com/klauspost/pgzip->fmt</title> +<path fill="none" stroke="#000000" d="M3370.0403,-11812.4518C3404.9118,-11770.4693 3488.9217,-11662.0024 3522,-11554.5998 3594.8989,-11317.9024 3481.7744,-10668.9578 3580,-10441.5998 3682.7275,-10203.8217 3876.0027,-10256.4874 3983,-10020.5998 4036.9727,-9901.611 4086.8645,-8927.8016 4094.8804,-8764.7257"/> +<polygon fill="#000000" stroke="#000000" points="4096.6296,-8764.7819 4095.1266,-8759.7022 4093.1338,-8764.6105 4096.6296,-8764.7819"/> +</g> +<!-- github.com/klauspost/pgzip->io --> +<g id="edge478" class="edge"> +<title>github.com/klauspost/pgzip->io</title> +<path fill="none" stroke="#000000" d="M3382.6101,-11848.7048C3424.5804,-11875.3239 3506.5643,-11925.7408 3580,-11961.5998 3753.4648,-12046.3037 3872.0574,-11965.6234 3983,-12123.5998 4086.1312,-12270.4532 4005.2817,-13561.7414 4041,-13737.5998 4050.0637,-13782.225 4071.3771,-13831.1456 4084.6518,-13858.9366"/> +<polygon fill="#000000" stroke="#000000" points="4083.1242,-13859.7979 4086.871,-13863.5428 4086.2773,-13858.2787 4083.1242,-13859.7979"/> +</g> +<!-- github.com/klauspost/pgzip->sync --> +<g id="edge479" class="edge"> +<title>github.com/klauspost/pgzip->sync</title> +<path fill="none" stroke="#000000" d="M3362.9029,-11812.3718C3392.1353,-11747.8427 3489.3797,-11523.5419 3522,-11326.5998 3539.7166,-11219.6375 3518.1917,-3609.6759 3580,-3520.5998 3689.3097,-3363.0662 3834.5207,-3487.9231 3983,-3366.5998 4035.7618,-3323.4878 4071.5388,-3248.9619 4087.1356,-3211.4988"/> +<polygon fill="#000000" stroke="#000000" points="4088.8326,-3211.9731 4089.114,-3206.6831 4085.5952,-3210.643 4088.8326,-3211.9731"/> +</g> +<!-- github.com/klauspost/pgzip->time --> +<g id="edge480" class="edge"> +<title>github.com/klauspost/pgzip->time</title> +<path fill="none" stroke="#000000" d="M3438.2131,-11827.8517C3570.0793,-11819.1667 3827.1634,-11783.9978 3983,-11643.5998 4055.179,-11578.5715 4083.3846,-11459.0838 4092.3971,-11408.8332"/> +<polygon fill="#000000" stroke="#000000" points="4094.1212,-11409.1326 4093.2588,-11403.9058 4090.6735,-11408.5296 4094.1212,-11409.1326"/> +</g> +<!-- github.com/klauspost/compress/flate --> +<g id="node113" class="node"> +<title>github.com/klauspost/compress/flate</title> +<g id="a_node113"><a xlink:href="https://godoc.org/github.com/klauspost/compress/flate" xlink:title="github.com/klauspost/compress/flate" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3878,-10491.5998C3878,-10491.5998 3685,-10491.5998 3685,-10491.5998 3679,-10491.5998 3673,-10485.5998 3673,-10479.5998 3673,-10479.5998 3673,-10467.5998 3673,-10467.5998 3673,-10461.5998 3679,-10455.5998 3685,-10455.5998 3685,-10455.5998 3878,-10455.5998 3878,-10455.5998 3884,-10455.5998 3890,-10461.5998 3890,-10467.5998 3890,-10467.5998 3890,-10479.5998 3890,-10479.5998 3890,-10485.5998 3884,-10491.5998 3878,-10491.5998"/> +<text text-anchor="middle" x="3781.5" y="-10469.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/flate</text> +</a> +</g> +</g> +<!-- github.com/klauspost/pgzip->github.com/klauspost/compress/flate --> +<g id="edge475" class="edge"> +<title>github.com/klauspost/pgzip->github.com/klauspost/compress/flate</title> +<path fill="none" stroke="#000000" d="M3370.0159,-11812.4443C3404.8368,-11770.4461 3488.7487,-11661.949 3522,-11554.5998 3556.5063,-11443.1989 3502.065,-10593.3579 3580,-10506.5998 3602.0927,-10482.006 3634.8306,-10470.9516 3667.5256,-10466.8359"/> +<polygon fill="#000000" stroke="#000000" points="3668.0031,-10468.5426 3672.7709,-10466.2339 3667.604,-10465.0655 3668.0031,-10468.5426"/> +</g> +<!-- github.com/klauspost/pgzip->hash/crc32 --> +<g id="edge477" class="edge"> +<title>github.com/klauspost/pgzip->hash/crc32</title> +<path fill="none" stroke="#000000" d="M3370.8306,-11848.85C3401.5378,-11883.4338 3469.5132,-11961.2677 3522,-12030.5998 3549.6047,-12067.064 3541.845,-12090.3838 3580,-12115.5998 3627.8919,-12147.2508 3695.2162,-12154.9139 3738.6049,-12156.2413"/> +<polygon fill="#000000" stroke="#000000" points="3738.7317,-12157.995 3743.7746,-12156.3729 3738.8208,-12154.4961 3738.7317,-12157.995"/> +</g> +<!-- github.com/klauspost/pgzip->hash --> +<g id="edge476" class="edge"> +<title>github.com/klauspost/pgzip->hash</title> +<path fill="none" stroke="#000000" d="M3438.1181,-11830.3743C3601.0712,-11829.9348 3954.7033,-11828.9809 4063.4933,-11828.6875"/> +<polygon fill="#000000" stroke="#000000" points="4063.7964,-11830.4368 4068.7916,-11828.6732 4063.7869,-11826.9368 4063.7964,-11830.4368"/> +</g> +<!-- github.com/ulikunitz/xz->bytes --> +<g id="edge625" class="edge"> +<title>github.com/ulikunitz/xz->bytes</title> +<path fill="none" stroke="#000000" d="M3030.5591,-13213.2406C3064.8423,-13213.9657 3103.5828,-13221.4064 3129,-13246.5998 3244.7249,-13361.3058 3068.7345,-13496.515 3187,-13608.5998 3441.1923,-13849.5078 3932.6623,-13746.3993 4063.3997,-13713.3736"/> +<polygon fill="#000000" stroke="#000000" points="4064.2137,-13714.9722 4068.6268,-13712.0418 4063.3495,-13711.5806 4064.2137,-13714.9722"/> +</g> +<!-- github.com/ulikunitz/xz->crypto/sha256 --> +<g id="edge626" class="edge"> +<title>github.com/ulikunitz/xz->crypto/sha256</title> +<path fill="none" stroke="#000000" d="M2959.5047,-13238.9553C2976.8536,-13366.8048 3079.9022,-14137.7625 3129,-14768.5998 3140.1935,-14912.42 3111.9561,-15942.401 3187,-16065.5998 3388.4719,-16396.3542 3883.1555,-16537.9306 4044.3232,-16576.2844"/> +<polygon fill="#000000" stroke="#000000" points="4044.1862,-16578.0502 4049.4547,-16577.4974 4044.9914,-16574.644 4044.1862,-16578.0502"/> +</g> +<!-- github.com/ulikunitz/xz->errors --> +<g id="edge627" class="edge"> +<title>github.com/ulikunitz/xz->errors</title> +<path fill="none" stroke="#000000" d="M2961.7276,-13202.4414C2985.5576,-13109.895 3092.7897,-12681.1083 3129,-12322.5998 3142.4054,-12189.8765 3118.5115,-7635.0748 3187,-7520.5998 3274.323,-7374.644 3425.6294,-7475.7465 3522,-7335.5998 3627.7728,-7181.7801 3448.1298,-7051.731 3580,-6919.5998 3707.7847,-6791.5622 3814.8951,-6929.4079 3983,-6862.5998 4017.2767,-6848.9776 4051.0619,-6822.9425 4072.6294,-6804.2698"/> +<polygon fill="#000000" stroke="#000000" points="4073.9007,-6805.4828 4076.5117,-6800.8735 4071.5962,-6802.8485 4073.9007,-6805.4828"/> +</g> +<!-- github.com/ulikunitz/xz->fmt --> +<g id="edge628" class="edge"> +<title>github.com/ulikunitz/xz->fmt</title> +<path fill="none" stroke="#000000" d="M2961.0521,-13202.3563C2981.5826,-13109.3947 3075.2447,-12678.9 3129,-12322.5998 3163.9551,-12090.9107 3089.0607,-12011.4604 3187,-11798.5998 3278.0833,-11600.64 3437.8496,-11641.6047 3522,-11440.5998 3583.1089,-11294.6328 3484.2659,-10143.5984 3580,-10017.5998 3693.8576,-9867.7482 3864.8781,-10034.1136 3983,-9887.5998 4056.1657,-9796.8479 4089.8088,-8920.2197 4095.2139,-8765.0234"/> +<polygon fill="#000000" stroke="#000000" points="4096.9702,-8764.8669 4095.3943,-8759.8094 4093.4723,-8764.7458 4096.9702,-8764.8669"/> +</g> +<!-- github.com/ulikunitz/xz->io --> +<g id="edge634" class="edge"> +<title>github.com/ulikunitz/xz->io</title> +<path fill="none" stroke="#000000" d="M2959.7612,-13238.6286C2980.2146,-13371.1816 3109.5246,-14192.0032 3187,-14269.5998 3294.734,-14377.5024 3383.6067,-14279.593 3522,-14343.5998 3551.3672,-14357.1822 3549.3326,-14377.2836 3580,-14387.5998 3749.7635,-14444.7063 3848.8632,-14506.2932 3983,-14387.5998 4062.3076,-14317.4231 3983.3026,-14003.4001 4041,-13914.5998 4046.5798,-13906.0122 4055.2489,-13899.3357 4064.0675,-13894.3056"/> +<polygon fill="#000000" stroke="#000000" points="4065.3849,-13895.5848 4068.9634,-13891.6788 4063.7301,-13892.5007 4065.3849,-13895.5848"/> +</g> +<!-- github.com/ulikunitz/xz->hash/crc32 --> +<g id="edge632" class="edge"> +<title>github.com/ulikunitz/xz->hash/crc32</title> +<path fill="none" stroke="#000000" d="M2970.9648,-13202.5616C3069.2419,-13075.6179 3656.1197,-12317.5526 3764.3479,-12177.7551"/> +<polygon fill="#000000" stroke="#000000" points="3765.7322,-12178.8256 3767.4093,-12173.8006 3762.9647,-12176.683 3765.7322,-12178.8256"/> +</g> +<!-- github.com/ulikunitz/xz->hash --> +<g id="edge631" class="edge"> +<title>github.com/ulikunitz/xz->hash</title> +<path fill="none" stroke="#000000" d="M3030.8434,-13222.2892C3223.8082,-13222.2512 3744.6343,-13192.6807 3983,-12888.5998 4038.3927,-12817.9359 4028.245,-12171.4764 4041,-12082.5998 4053.3742,-11996.3763 4078.3646,-11895.7455 4089.9049,-11851.4752"/> +<polygon fill="#000000" stroke="#000000" points="4091.6047,-11851.8921 4091.1778,-11846.6119 4088.2187,-11851.0058 4091.6047,-11851.8921"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog --> +<g id="node132" class="node"> +<title>github.com/ulikunitz/xz/internal/xlog</title> +<g id="a_node132"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/internal/xlog" xlink:title="github.com/ulikunitz/xz/internal/xlog" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3879.5,-13300.5998C3879.5,-13300.5998 3683.5,-13300.5998 3683.5,-13300.5998 3677.5,-13300.5998 3671.5,-13294.5998 3671.5,-13288.5998 3671.5,-13288.5998 3671.5,-13276.5998 3671.5,-13276.5998 3671.5,-13270.5998 3677.5,-13264.5998 3683.5,-13264.5998 3683.5,-13264.5998 3879.5,-13264.5998 3879.5,-13264.5998 3885.5,-13264.5998 3891.5,-13270.5998 3891.5,-13276.5998 3891.5,-13276.5998 3891.5,-13288.5998 3891.5,-13288.5998 3891.5,-13294.5998 3885.5,-13300.5998 3879.5,-13300.5998"/> +<text text-anchor="middle" x="3781.5" y="-13278.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/internal/xlog</text> +</a> +</g> +</g> +<!-- github.com/ulikunitz/xz->github.com/ulikunitz/xz/internal/xlog --> +<g id="edge629" class="edge"> +<title>github.com/ulikunitz/xz->github.com/ulikunitz/xz/internal/xlog</title> +<path fill="none" stroke="#000000" d="M3030.7026,-13222.8748C3138.6731,-13226.579 3346.1156,-13235.1048 3522,-13250.5998 3569.2411,-13254.7617 3621.1426,-13260.8561 3666.0329,-13266.6125"/> +<polygon fill="#000000" stroke="#000000" points="3666.0086,-13268.3738 3671.1911,-13267.2765 3666.4555,-13264.9024 3666.0086,-13268.3738"/> +</g> +<!-- github.com/ulikunitz/xz/lzma --> +<g id="node133" class="node"> +<title>github.com/ulikunitz/xz/lzma</title> +<g id="a_node133"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/lzma" xlink:title="github.com/ulikunitz/xz/lzma" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3431.5,-13365.5998C3431.5,-13365.5998 3277.5,-13365.5998 3277.5,-13365.5998 3271.5,-13365.5998 3265.5,-13359.5998 3265.5,-13353.5998 3265.5,-13353.5998 3265.5,-13341.5998 3265.5,-13341.5998 3265.5,-13335.5998 3271.5,-13329.5998 3277.5,-13329.5998 3277.5,-13329.5998 3431.5,-13329.5998 3431.5,-13329.5998 3437.5,-13329.5998 3443.5,-13335.5998 3443.5,-13341.5998 3443.5,-13341.5998 3443.5,-13353.5998 3443.5,-13353.5998 3443.5,-13359.5998 3437.5,-13365.5998 3431.5,-13365.5998"/> +<text text-anchor="middle" x="3354.5" y="-13343.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/lzma</text> +</a> +</g> +</g> +<!-- github.com/ulikunitz/xz->github.com/ulikunitz/xz/lzma --> +<g id="edge630" class="edge"> +<title>github.com/ulikunitz/xz->github.com/ulikunitz/xz/lzma</title> +<path fill="none" stroke="#000000" d="M2993.9994,-13238.6309C3038.7538,-13259.791 3116.8341,-13294.6435 3187,-13315.5998 3210.4382,-13322.6001 3236.1948,-13328.415 3260.332,-13333.0805"/> +<polygon fill="#000000" stroke="#000000" points="3260.2171,-13334.8401 3265.4566,-13334.0587 3260.8734,-13331.4022 3260.2171,-13334.8401"/> +</g> +<!-- hash/crc64 --> +<g id="node134" class="node"> +<title>hash/crc64</title> +<g id="a_node134"><a xlink:href="https://godoc.org/hash/crc64" xlink:title="hash/crc64" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3380,-13300.5998C3380,-13300.5998 3329,-13300.5998 3329,-13300.5998 3323,-13300.5998 3317,-13294.5998 3317,-13288.5998 3317,-13288.5998 3317,-13276.5998 3317,-13276.5998 3317,-13270.5998 3323,-13264.5998 3329,-13264.5998 3329,-13264.5998 3380,-13264.5998 3380,-13264.5998 3386,-13264.5998 3392,-13270.5998 3392,-13276.5998 3392,-13276.5998 3392,-13288.5998 3392,-13288.5998 3392,-13294.5998 3386,-13300.5998 3380,-13300.5998"/> +<text text-anchor="middle" x="3354.5" y="-13278.8998" font-family="Times,serif" font-size="14.00" fill="#000000">hash/crc64</text> +</a> +</g> +</g> +<!-- github.com/ulikunitz/xz->hash/crc64 --> +<g id="edge633" class="edge"> +<title>github.com/ulikunitz/xz->hash/crc64</title> +<path fill="none" stroke="#000000" d="M3030.8351,-13232.1162C3113.4393,-13245.0004 3244.4559,-13265.4357 3311.6545,-13275.917"/> +<polygon fill="#000000" stroke="#000000" points="3311.5646,-13277.6741 3316.7746,-13276.7156 3312.104,-13274.2159 3311.5646,-13277.6741"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->bytes --> +<g id="edge287" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->bytes</title> +<path fill="none" stroke="#000000" d="M3418.309,-15418.5633C3564.2286,-15375.9285 3914.9533,-15265.1037 3983,-15166.5998 4066.2524,-15046.0842 4011.4295,-13993.0591 4041,-13849.5998 4050.2486,-13804.7307 4071.5011,-13755.4486 4084.7142,-13727.4426"/> +<polygon fill="#000000" stroke="#000000" points="4086.3548,-13728.0676 4086.9228,-13722.8007 4083.1943,-13726.5638 4086.3548,-13728.0676"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->encoding/json --> +<g id="edge288" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->encoding/json</title> +<path fill="none" stroke="#000000" d="M3361.6124,-15454.7227C3388.4731,-15524.121 3484.1329,-15780.1337 3522,-16000.5998 3543.1882,-16123.9594 3498.9565,-17030.2139 3580,-17125.5998 3696.7388,-17262.9981 3938.6726,-17255.3842 4044.5262,-17244.4781"/> +<polygon fill="#000000" stroke="#000000" points="4044.9814,-17246.1898 4049.7696,-17243.9236 4044.6132,-17242.7092 4044.9814,-17246.1898"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->fmt --> +<g id="edge289" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->fmt</title> +<path fill="none" stroke="#000000" d="M3361.9189,-15418.1565C3389.4625,-15348.6567 3486.1067,-15095.4049 3522,-14876.5998 3552.1994,-14692.5046 3493.9291,-13364.1138 3580,-13198.5998 3683.5109,-12999.5488 3880.3809,-13094.112 3983,-12894.5998 4040.5742,-12782.664 4037.5562,-10755.4273 4041,-10629.5998 4061.9466,-9864.2615 4090.4347,-8924.497 4095.2844,-8765.0896"/> +<polygon fill="#000000" stroke="#000000" points="4097.0439,-8764.8007 4095.4469,-8759.7498 4093.5455,-8764.6942 4097.0439,-8764.8007"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->io --> +<g id="edge291" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->io</title> +<path fill="none" stroke="#000000" d="M3502.3049,-15450.4121C3666.9235,-15462.174 3918.4421,-15468.139 3983,-15402.5998 4099.1113,-15284.7236 3956.6896,-14056.9669 4041,-13914.5998 4046.3638,-13905.5424 4055.2659,-13898.6744 4064.3617,-13893.6139"/> +<polygon fill="#000000" stroke="#000000" points="4065.2115,-13895.1443 4068.8278,-13891.2732 4063.5868,-13892.0443 4065.2115,-13895.1443"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->strings --> +<g id="edge294" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->strings</title> +<path fill="none" stroke="#000000" d="M3362.0703,-15418.1805C3390.1514,-15348.7659 3488.458,-15095.7777 3522,-14876.5998 3540.6377,-14754.8127 3499.7316,-10535.0688 3580,-10441.5998 3698.2931,-10303.8527 3863.9671,-10511.7081 3983,-10374.5998 4027.3005,-10323.5722 4038.0499,-8010.1102 4041,-7942.5998 4055.6182,-7608.0801 4086.378,-7201.5635 4094.1739,-7100.9515"/> +<polygon fill="#000000" stroke="#000000" points="4095.9278,-7100.9673 4094.5701,-7095.8468 4092.4383,-7100.6964 4095.9278,-7100.9673"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->os --> +<g id="edge292" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->os</title> +<path fill="none" stroke="#000000" d="M3361.7994,-15454.6919C3389.3357,-15523.9789 3487.1172,-15779.6419 3522,-16000.5998 3542.5382,-16130.6947 3516.4705,-18255.2287 3580,-18370.5998 3683.8743,-18559.2382 3848.1416,-18471.7084 3983,-18639.5998 4027.502,-18695.0024 3994.8121,-18734.5947 4041,-18788.5998 4047.3007,-18795.9669 4055.7441,-18802.0432 4064.121,-18806.8482"/> +<polygon fill="#000000" stroke="#000000" points="4063.5281,-18808.5188 4068.754,-18809.3869 4065.2101,-18805.4494 4063.5281,-18808.5188"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->github.com/docker/docker-credential-helpers/credentials --> +<g id="edge290" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->github.com/docker/docker-credential-helpers/credentials</title> +<path fill="none" stroke="#000000" d="M3472.8763,-15454.6196C3530.3761,-15463.3725 3599.4467,-15473.8868 3657.6535,-15482.7473"/> +<polygon fill="#000000" stroke="#000000" points="3657.687,-15484.5225 3662.8935,-15483.545 3658.2138,-15481.0624 3657.687,-15484.5225"/> +</g> +<!-- os/exec --> +<g id="node99" class="node"> +<title>os/exec</title> +<g id="a_node99"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3798,-19490.5998C3798,-19490.5998 3765,-19490.5998 3765,-19490.5998 3759,-19490.5998 3753,-19484.5998 3753,-19478.5998 3753,-19478.5998 3753,-19466.5998 3753,-19466.5998 3753,-19460.5998 3759,-19454.5998 3765,-19454.5998 3765,-19454.5998 3798,-19454.5998 3798,-19454.5998 3804,-19454.5998 3810,-19460.5998 3810,-19466.5998 3810,-19466.5998 3810,-19478.5998 3810,-19478.5998 3810,-19484.5998 3804,-19490.5998 3798,-19490.5998"/> +<text text-anchor="middle" x="3781.5" y="-19468.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text> +</a> +</g> +</g> +<!-- github.com/docker/docker-credential-helpers/client->os/exec --> +<g id="edge293" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->os/exec</title> +<path fill="none" stroke="#000000" d="M3361.8182,-15454.689C3389.4222,-15523.9653 3487.4164,-15779.5949 3522,-16000.5998 3613.5805,-16585.8402 3501.3166,-18078.4864 3580,-18665.5998 3622.4466,-18982.324 3742.074,-19354.4879 3773.7328,-19449.5844"/> +<polygon fill="#000000" stroke="#000000" points="3772.104,-19450.232 3775.3468,-19454.421 3775.424,-19449.1241 3772.104,-19450.232"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->bufio --> +<g id="edge295" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->bufio</title> +<path fill="none" stroke="#000000" d="M3943.3471,-15490.6342C3958.4301,-15483.7586 3972.112,-15474.345 3983,-15461.5998 4090.0389,-15336.3029 3957.0178,-14121.3871 4041,-13979.5998 4046.3646,-13970.5428 4055.2668,-13963.6749 4064.3625,-13958.6144"/> +<polygon fill="#000000" stroke="#000000" points="4065.2124,-13960.1448 4068.8286,-13956.2737 4063.5876,-13957.0448 4065.2124,-13960.1448"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->bytes --> +<g id="edge296" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->bytes</title> +<path fill="none" stroke="#000000" d="M3861.6228,-15483.5694C3905.2657,-15469.2944 3955.7845,-15444.5295 3983,-15402.5998 4077.0122,-15257.7597 4006.7053,-14018.8358 4041,-13849.5998 4050.0987,-13804.7001 4071.4005,-13755.428 4084.6636,-13727.4323"/> +<polygon fill="#000000" stroke="#000000" points="4086.3041,-13728.0581 4086.8808,-13722.7921 4083.1461,-13726.549 4086.3041,-13728.0581"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->encoding/json --> +<g id="edge297" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->encoding/json</title> +<path fill="none" stroke="#000000" d="M3784.2468,-15519.8167C3811.8246,-15702.6926 4037.8386,-17200.9538 4041,-17205.5998 4043.6355,-17209.4729 4046.907,-17212.9409 4050.5315,-17216.0317"/> +<polygon fill="#000000" stroke="#000000" points="4049.9187,-17217.7758 4054.9316,-17219.4886 4052.081,-17215.0235 4049.9187,-17217.7758"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->fmt --> +<g id="edge298" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->fmt</title> +<path fill="none" stroke="#000000" d="M3791.562,-15483.5958C3826.5737,-15419.8104 3943.1069,-15197.678 3983,-14997.5998 4030.4548,-14759.5966 4035.8553,-10872.2333 4041,-10629.5998 4057.2303,-9864.1469 4089.605,-8924.4769 4095.1749,-8765.087"/> +<polygon fill="#000000" stroke="#000000" points="4096.9357,-8764.8058 4095.3617,-8759.7477 4093.4379,-8764.6834 4096.9357,-8764.8058"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->io --> +<g id="edge299" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->io</title> +<path fill="none" stroke="#000000" d="M3943.368,-15490.6521C3958.4472,-15483.7731 3972.1225,-15474.3539 3983,-15461.5998 4094.6191,-15330.724 3953.4615,-14062.6685 4041,-13914.5998 4046.3571,-13905.5384 4055.2574,-13898.6694 4064.3536,-13893.6092"/> +<polygon fill="#000000" stroke="#000000" points="4065.2036,-13895.1395 4068.8201,-13891.2687 4063.5791,-13892.0394 4065.2036,-13895.1395"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->strings --> +<g id="edge301" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->strings</title> +<path fill="none" stroke="#000000" d="M3791.8428,-15483.1586C3827.2693,-15418.8158 3943.7039,-15197.3159 3983,-14997.5998 4058.6707,-14613.0159 4026.9476,-8334.3055 4041,-7942.5998 4053.0046,-7607.9761 4085.6943,-7201.5363 4094.0398,-7100.9462"/> +<polygon fill="#000000" stroke="#000000" points="4095.7937,-7100.9704 4094.4641,-7095.8426 4092.3057,-7100.6804 4095.7937,-7100.9704"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->os --> +<g id="edge300" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->os</title> +<path fill="none" stroke="#000000" d="M3785.1222,-15519.883C3808.1068,-15636.801 3934.2438,-16292.0039 3983,-16832.5998 3992.7653,-16940.8748 3985.4095,-18695.1733 4041,-18788.5998 4046.34,-18797.5743 4055.2358,-18804.2843 4064.3332,-18809.1784"/> +<polygon fill="#000000" stroke="#000000" points="4063.5489,-18810.7428 4068.8007,-18811.4371 4065.1282,-18807.6193 4063.5489,-18810.7428"/> +</g> +<!-- github.com/docker/docker/pkg/homedir->os --> +<g id="edge306" class="edge"> +<title>github.com/docker/docker/pkg/homedir->os</title> +<path fill="none" stroke="#000000" d="M1848.6097,-15546.9899C1849.781,-15678.3161 1863.454,-16486.6598 2023,-17117.5998 2122.8506,-17512.4687 2273.4782,-17570.4216 2368,-17966.5998 2440.6527,-18271.116 2345.8636,-18365.967 2426,-18668.5998 2522.8599,-19034.3885 2549.5816,-19144.3543 2785,-19440.5998 3030.4645,-19749.4872 3187.4928,-19795.5713 3580,-19835.5998 3758.1869,-19853.7716 3851.1736,-19956.854 3983,-19835.5998 4039.8704,-19783.2903 4031.264,-19219.2532 4041,-19142.5998 4055.3007,-19030.0076 4080.8544,-18896.6696 4091.3275,-18843.8298"/> +<polygon fill="#000000" stroke="#000000" points="4093.0938,-18843.9199 4092.3521,-18838.6746 4089.6609,-18843.2375 4093.0938,-18843.9199"/> +</g> +<!-- github.com/docker/docker/pkg/idtools --> +<g id="node100" class="node"> +<title>github.com/docker/docker/pkg/idtools</title> +<g id="a_node100"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/idtools" xlink:title="github.com/docker/docker/pkg/idtools" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2295.5,-15166.5998C2295.5,-15166.5998 2095.5,-15166.5998 2095.5,-15166.5998 2089.5,-15166.5998 2083.5,-15160.5998 2083.5,-15154.5998 2083.5,-15154.5998 2083.5,-15142.5998 2083.5,-15142.5998 2083.5,-15136.5998 2089.5,-15130.5998 2095.5,-15130.5998 2095.5,-15130.5998 2295.5,-15130.5998 2295.5,-15130.5998 2301.5,-15130.5998 2307.5,-15136.5998 2307.5,-15142.5998 2307.5,-15142.5998 2307.5,-15154.5998 2307.5,-15154.5998 2307.5,-15160.5998 2301.5,-15166.5998 2295.5,-15166.5998"/> +<text text-anchor="middle" x="2195.5" y="-15144.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/idtools</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/homedir->github.com/docker/docker/pkg/idtools --> +<g id="edge304" class="edge"> +<title>github.com/docker/docker/pkg/homedir->github.com/docker/docker/pkg/idtools</title> +<path fill="none" stroke="#000000" d="M1865.0031,-15510.5273C1922.3157,-15447.7641 2112.6467,-15239.3325 2175.3054,-15170.715"/> +<polygon fill="#000000" stroke="#000000" points="2176.8165,-15171.6553 2178.8959,-15166.783 2174.232,-15169.2952 2176.8165,-15171.6553"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user --> +<g id="node101" class="node"> +<title>github.com/opencontainers/runc/libcontainer/user</title> +<g id="a_node101"><a xlink:href="https://godoc.org/github.com/opencontainers/runc/libcontainer/user" xlink:title="github.com/opencontainers/runc/libcontainer/user" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M3486,-14861.5998C3486,-14861.5998 3223,-14861.5998 3223,-14861.5998 3217,-14861.5998 3211,-14855.5998 3211,-14849.5998 3211,-14849.5998 3211,-14837.5998 3211,-14837.5998 3211,-14831.5998 3217,-14825.5998 3223,-14825.5998 3223,-14825.5998 3486,-14825.5998 3486,-14825.5998 3492,-14825.5998 3498,-14831.5998 3498,-14837.5998 3498,-14837.5998 3498,-14849.5998 3498,-14849.5998 3498,-14855.5998 3492,-14861.5998 3486,-14861.5998"/> +<text text-anchor="middle" x="3354.5" y="-14839.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/runc/libcontainer/user</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/homedir->github.com/opencontainers/runc/libcontainer/user --> +<g id="edge305" class="edge"> +<title>github.com/docker/docker/pkg/homedir->github.com/opencontainers/runc/libcontainer/user</title> +<path fill="none" stroke="#000000" d="M1850.4456,-15510.3956C1858.364,-15445.7497 1894.1661,-15225.2986 2023,-15116.5998 2366.8589,-14826.4816 2937.0652,-14817.075 3205.7482,-14831.1744"/> +<polygon fill="#000000" stroke="#000000" points="3205.7741,-14832.9282 3210.8604,-14831.4474 3205.9608,-14829.4332 3205.7741,-14832.9282"/> +</g> +<!-- github.com/docker/go-connections/sockets->crypto/tls --> +<g id="edge351" class="edge"> +<title>github.com/docker/go-connections/sockets->crypto/tls</title> +<path fill="none" stroke="#000000" d="M2959.9564,-9464.6054C2979.6311,-9585.423 3091.9734,-10290.9441 3129,-10870.5998 3135.9619,-10979.589 3134.2587,-18637.9679 3187,-18733.5998 3284.85,-18911.0239 3896.2286,-19178.4036 4057.198,-19246.418"/> +<polygon fill="#000000" stroke="#000000" points="4056.541,-19248.0402 4061.828,-19248.3719 4057.9018,-19244.8155 4056.541,-19248.0402"/> +</g> +<!-- github.com/docker/go-connections/sockets->errors --> +<g id="edge352" class="edge"> +<title>github.com/docker/go-connections/sockets->errors</title> +<path fill="none" stroke="#000000" d="M2959.7777,-9428.5488C2977.6557,-9311.7473 3077.4019,-8650.6719 3129,-8108.5998 3138.8633,-8004.9796 3125.8949,-7257.8648 3187,-7173.5998 3281.0416,-7043.9148 3415.7986,-7160.5301 3522,-7040.5998 3589.3222,-6964.5747 3501.8664,-6884.4631 3580,-6819.5998 3723.9524,-6700.0965 3975.2941,-6750.5781 4063.9397,-6773.5326"/> +<polygon fill="#000000" stroke="#000000" points="4063.5108,-6775.2292 4068.7911,-6774.8037 4064.3979,-6771.8435 4063.5108,-6775.2292"/> +</g> +<!-- github.com/docker/go-connections/sockets->fmt --> +<g id="edge353" class="edge"> +<title>github.com/docker/go-connections/sockets->fmt</title> +<path fill="none" stroke="#000000" d="M3081.655,-9461.5215C3209.4206,-9469.3862 3405.415,-9459.6491 3522,-9347.5998 3609.8663,-9263.1519 3492.4781,-9164.4047 3580,-9079.5998 3710.7822,-8952.8777 3840.7129,-9119.2522 3983,-9005.5998 4060.9579,-8943.3307 4086.0866,-8817.0814 4093.3419,-8764.9387"/> +<polygon fill="#000000" stroke="#000000" points="4095.0963,-8765.0228 4094.0276,-8759.8343 4091.6274,-8764.5567 4095.0963,-8765.0228"/> +</g> +<!-- github.com/docker/go-connections/sockets->strings --> +<g id="edge359" class="edge"> +<title>github.com/docker/go-connections/sockets->strings</title> +<path fill="none" stroke="#000000" d="M2996.6182,-9428.4042C3122.1656,-9370.5936 3504.0068,-9193.5572 3522,-9170.5998 3597.1774,-9074.6816 3491.6651,-8986.5575 3580,-8902.5998 3711.556,-8777.5628 3861.1141,-8971.0806 3983,-8836.5998 3983.9573,-8835.5436 4081.218,-7309.5942 4094.5032,-7101.0941"/> +<polygon fill="#000000" stroke="#000000" points="4096.2651,-7100.9617 4094.8366,-7095.8605 4092.7721,-7100.7391 4096.2651,-7100.9617"/> +</g> +<!-- github.com/docker/go-connections/sockets->sync --> +<g id="edge360" class="edge"> +<title>github.com/docker/go-connections/sockets->sync</title> +<path fill="none" stroke="#000000" d="M2959.6048,-9428.1923C2978.0813,-9296.6608 3089.7299,-8485.7022 3129,-7821.5998 3136.5039,-7694.7002 3132.2342,-3360.3192 3187,-3245.5998 3270.3533,-3070.9971 3409.8832,-3125.2821 3522,-2967.5998 3564.623,-2907.6544 3519.559,-2854.5171 3580,-2812.5998 3653.59,-2761.5634 3911.1372,-2759.1589 3983,-2812.5998 4041.6217,-2856.194 4081.2984,-3090.4023 4092.6559,-3165.4479"/> +<polygon fill="#000000" stroke="#000000" points="4090.952,-3165.8863 4093.4246,-3170.5713 4094.4132,-3165.3669 4090.952,-3165.8863"/> +</g> +<!-- github.com/docker/go-connections/sockets->time --> +<g id="edge362" class="edge"> +<title>github.com/docker/go-connections/sockets->time</title> +<path fill="none" stroke="#000000" d="M2965.9244,-9464.8982C2994.0386,-9523.251 3081.6997,-9710.7162 3129,-9874.5998 3168.9599,-10013.0507 3085.8545,-10090.9595 3187,-10193.5998 3293.5142,-10301.6881 3416.742,-10150.2879 3522,-10259.5998 3636.5419,-10378.5531 3463.1342,-10509.9289 3580,-10626.5998 3708.2959,-10754.6817 3859.2391,-10557.1309 3983,-10689.5998 4084.117,-10797.8316 3961.6342,-11228.5405 4041,-11353.5998 4046.444,-11362.1781 4055.0749,-11368.7229 4063.9003,-11373.5881"/> +<polygon fill="#000000" stroke="#000000" points="4063.5587,-11375.3812 4068.8043,-11376.1204 4065.1646,-11372.2713 4063.5587,-11375.3812"/> +</g> +<!-- github.com/docker/go-connections/sockets->net/http --> +<g id="edge356" class="edge"> +<title>github.com/docker/go-connections/sockets->net/http</title> +<path fill="none" stroke="#000000" d="M2959.6206,-9428.1932C2978.2068,-9296.6681 3090.4772,-8485.7459 3129,-7821.5998 3137.9502,-7667.2952 3136.83,-2403.7948 3187,-2257.5998 3266.7793,-2025.1237 3434.2403,-2047.1823 3522,-1817.5998 3579.9474,-1666.0074 3497.3539,-1229.2702 3580,-1089.5998 3689.165,-905.113 3843.0401,-986.9686 3983,-824.5998 4042.431,-755.6533 4076.8839,-650.1833 4089.9308,-603.8247"/> +<polygon fill="#000000" stroke="#000000" points="4091.6928,-604.0196 4091.3441,-598.7337 4088.3203,-603.0833 4091.6928,-604.0196"/> +</g> +<!-- github.com/docker/go-connections/sockets->net/url --> +<g id="edge357" class="edge"> +<title>github.com/docker/go-connections/sockets->net/url</title> +<path fill="none" stroke="#000000" d="M2959.61,-9428.1926C2978.1227,-9296.6632 3089.9766,-8485.7167 3129,-7821.5998 3144.8422,-7551.9901 3125.654,-3223.6151 3187,-2960.5998 3265.3429,-2624.7123 3431.9452,-2593.5385 3522,-2260.5998 3563.102,-2108.643 3467.3672,-1664.573 3580,-1554.5998 3714.2202,-1423.5491 3973.3152,-1486.5239 4063.8399,-1514.0322"/> +<polygon fill="#000000" stroke="#000000" points="4063.4979,-1515.7578 4068.7914,-1515.5528 4064.5254,-1512.412 4063.4979,-1515.7578"/> +</g> +<!-- github.com/docker/go-connections/sockets->os --> +<g id="edge358" class="edge"> +<title>github.com/docker/go-connections/sockets->os</title> +<path fill="none" stroke="#000000" d="M2959.956,-9464.6054C2979.6279,-9585.4232 3091.9559,-10290.9453 3129,-10870.5998 3135.9085,-10978.7017 3118.8719,-18585.3839 3187,-18669.5998 3243.8024,-18739.8156 3907.7144,-18803.6522 4063.4514,-18817.7268"/> +<polygon fill="#000000" stroke="#000000" points="4063.6383,-18819.5006 4068.7752,-18818.2064 4063.9524,-18816.0147 4063.6383,-18819.5006"/> +</g> +<!-- github.com/docker/go-connections/sockets->net --> +<g id="edge355" class="edge"> +<title>github.com/docker/go-connections/sockets->net</title> +<path fill="none" stroke="#000000" d="M2959.9304,-9464.607C2979.436,-9585.4356 3090.8756,-10291.0153 3129,-10870.5998 3147.9447,-11158.6065 3122.7005,-15784.224 3187,-16065.5998 3265.1783,-16407.7091 3414.0314,-16446.6935 3522,-16780.5998 3564.5875,-16912.3071 3481.0422,-16989.8121 3580,-17086.5998 3715.5928,-17219.2192 3973.3546,-17192.6948 4063.7225,-17178.4215"/> +<polygon fill="#000000" stroke="#000000" points="4064.2632,-17180.107 4068.9214,-17177.5844 4063.7068,-17176.6515 4064.2632,-17180.107"/> +</g> +<!-- github.com/docker/go-connections/sockets->syscall --> +<g id="edge361" class="edge"> +<title>github.com/docker/go-connections/sockets->syscall</title> +<path fill="none" stroke="#000000" d="M2959.9554,-9464.6054C2979.6239,-9585.4234 3091.9333,-10290.9467 3129,-10870.5998 3135.8409,-10977.5788 3118.0802,-18506.494 3187,-18588.5998 3302.8413,-18726.6043 3915.0341,-18750.9984 4063.531,-18754.9072"/> +<polygon fill="#000000" stroke="#000000" points="4063.573,-18756.6587 4068.6163,-18755.0377 4063.6629,-18753.1599 4063.573,-18756.6587"/> +</g> +<!-- golang.org/x/net/proxy --> +<g id="node106" class="node"> +<title>golang.org/x/net/proxy</title> +<g id="a_node106"><a xlink:href="https://godoc.org/golang.org/x/net/proxy" xlink:title="golang.org/x/net/proxy" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3413,-7570.5998C3413,-7570.5998 3296,-7570.5998 3296,-7570.5998 3290,-7570.5998 3284,-7564.5998 3284,-7558.5998 3284,-7558.5998 3284,-7546.5998 3284,-7546.5998 3284,-7540.5998 3290,-7534.5998 3296,-7534.5998 3296,-7534.5998 3413,-7534.5998 3413,-7534.5998 3419,-7534.5998 3425,-7540.5998 3425,-7546.5998 3425,-7546.5998 3425,-7558.5998 3425,-7558.5998 3425,-7564.5998 3419,-7570.5998 3413,-7570.5998"/> +<text text-anchor="middle" x="3354.5" y="-7548.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/proxy</text> +</a> +</g> +</g> +<!-- github.com/docker/go-connections/sockets->golang.org/x/net/proxy --> +<g id="edge354" class="edge"> +<title>github.com/docker/go-connections/sockets->golang.org/x/net/proxy</title> +<path fill="none" stroke="#000000" d="M2972.3057,-9428.3133C3006.7608,-9386.0434 3090.4082,-9277.0201 3129,-9170.5998 3242.8797,-8856.5662 3338.1129,-7750.5161 3352.5947,-7575.8921"/> +<polygon fill="#000000" stroke="#000000" points="3354.3505,-7575.8938 3353.0188,-7570.7665 3350.8624,-7575.6052 3354.3505,-7575.8938"/> +</g> +<!-- github.com/docker/distribution->context --> +<g id="edge203" class="edge"> +<title>github.com/docker/distribution->context</title> +<path fill="none" stroke="#000000" d="M2605.1925,-5756.4673C2641.3647,-5731.7305 2701.8558,-5683.6585 2727,-5624.5998 2867.6519,-5294.237 2650.4357,-2726.4887 2785,-2393.5998 2869.0281,-2185.7289 3029.4816,-2225.5155 3129,-2024.5998 3196.8464,-1887.6261 3133.2271,-1828.6852 3187,-1685.5998 3287.2879,-1418.7417 3400.0854,-1396.2968 3522,-1138.5998 3555.601,-1067.5758 3517.644,-1019.4032 3580,-971.5998 3727.6061,-858.442 3974.0576,-904.1454 4062.8497,-925.7422"/> +<polygon fill="#000000" stroke="#000000" points="4062.4415,-927.4438 4067.7149,-926.94 4063.2783,-924.0453 4062.4415,-927.4438"/> +</g> +<!-- github.com/docker/distribution->errors --> +<g id="edge204" class="edge"> +<title>github.com/docker/distribution->errors</title> +<path fill="none" stroke="#000000" d="M2645.8994,-5756.5222C2671.4335,-5749.7742 2700.5649,-5741.962 2727,-5734.5998 2932.1354,-5677.4695 2976.1929,-5630.6792 3187,-5600.5998 3539.2686,-5550.3357 3741.87,-5424.9207 3983,-5686.5998 4058.4217,-5768.449 4090.1042,-6607.3592 4095.2389,-6759.1277"/> +<polygon fill="#000000" stroke="#000000" points="4093.4934,-6759.2959 4095.4103,-6764.2343 4096.9915,-6759.1784 4093.4934,-6759.2959"/> +</g> +<!-- github.com/docker/distribution->fmt --> +<g id="edge205" class="edge"> +<title>github.com/docker/distribution->fmt</title> +<path fill="none" stroke="#000000" d="M2578.4504,-5792.8643C2590.9802,-5907.073 2663.5011,-6522.608 2785,-6659.5998 2893.0381,-6781.4144 3032.5626,-6654.4097 3129,-6785.5998 3208.3925,-6893.6026 3097.9474,-7890.413 3187,-7990.5998 3287.2875,-8103.4261 3402.1948,-7954.7627 3522,-8046.5998 3571.2477,-8084.3508 3530.2368,-8136.531 3580,-8173.5998 3725.0196,-8281.6256 3850.1736,-8106.8912 3983,-8229.5998 4056.5875,-8297.582 4087.171,-8627.1798 4094.2992,-8718.2467"/> +<polygon fill="#000000" stroke="#000000" points="4092.5674,-8718.5507 4094.6974,-8723.401 4096.057,-8718.2811 4092.5674,-8718.5507"/> +</g> +<!-- github.com/docker/distribution->io --> +<g id="edge209" class="edge"> +<title>github.com/docker/distribution->io</title> +<path fill="none" stroke="#000000" d="M2579.7836,-5792.8339C2598.7705,-5899.2701 2694.773,-6451.2519 2727,-6906.5998 2751.8946,-7258.3458 2720.6573,-12906.8939 2785,-13253.5998 2796.6344,-13316.2908 3138.4456,-14287.272 3187,-14328.5998 3302.6245,-14427.0152 3384.5735,-14331.0353 3522,-14395.5998 3552.0299,-14409.7082 3548.7261,-14431.5192 3580,-14442.5998 3748.8274,-14502.4169 3849.6138,-14562.1361 3983,-14442.5998 4070.9051,-14363.8222 3977.3129,-14013.9839 4041,-13914.5998 4046.5255,-13905.9772 4055.1794,-13899.2909 4064.0007,-13894.2625"/> +<polygon fill="#000000" stroke="#000000" points="4065.319,-13895.5416 4068.8999,-13891.6378 4063.6661,-13892.4565 4065.319,-13895.5416"/> +</g> +<!-- github.com/docker/distribution->strings --> +<g id="edge212" class="edge"> +<title>github.com/docker/distribution->strings</title> +<path fill="none" stroke="#000000" d="M2669.1699,-5766.6858C2788.0336,-5761.5496 2994.8174,-5770.1394 3129,-5873.5998 3177.5015,-5910.9965 3143.1005,-5955.8944 3187,-5998.5998 3463.1245,-6267.2135 3752.067,-6033.2688 3983,-6341.5998 4046.6158,-6426.5367 4023.639,-6710.9106 4041,-6815.5998 4055.7529,-6904.5617 4079.7361,-7009.1713 4090.484,-7054.6089"/> +<polygon fill="#000000" stroke="#000000" points="4088.8097,-7055.1332 4091.6664,-7059.5944 4092.2152,-7054.3255 4088.8097,-7055.1332"/> +</g> +<!-- github.com/docker/distribution->time --> +<g id="edge213" class="edge"> +<title>github.com/docker/distribution->time</title> +<path fill="none" stroke="#000000" d="M2579.691,-5792.8408C2598.1569,-5899.3156 2691.7229,-6451.4781 2727,-6906.5998 2749.099,-7191.7062 2703.4738,-9205.5059 2785,-9479.5998 2867.6286,-9757.3999 3013.5823,-9767.7443 3129,-10033.5998 3168.4132,-10124.385 3124.5871,-10171.7885 3187,-10248.5998 3292.2892,-10378.1788 3422.8183,-10284.2885 3522,-10418.5998 3612.0212,-10540.506 3470.074,-10650.2867 3580,-10754.5998 3710.8426,-10878.7615 3856.329,-10674.1851 3983,-10802.5998 4069.4625,-10890.2525 3974.1082,-11250.2352 4041,-11353.5998 4046.5199,-11362.1295 4055.1722,-11368.6605 4063.9938,-11373.5281"/> +<polygon fill="#000000" stroke="#000000" points="4063.6483,-11375.3197 4068.8933,-11376.0633 4065.2568,-11372.2112 4063.6483,-11375.3197"/> +</g> +<!-- github.com/docker/distribution->github.com/opencontainers/go-digest --> +<g id="edge207" class="edge"> +<title>github.com/docker/distribution->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2579.7187,-5792.8387C2598.3405,-5899.3016 2692.6354,-6451.4083 2727,-6906.5998 2740.0927,-7080.025 2715.2341,-9877.2874 2785,-10036.5998 2884.2374,-10263.2112 2998.3321,-10261.5835 3187,-10421.5998 3328.8849,-10541.9378 3415.3343,-10520.1696 3522,-10672.5998 3573.1081,-10745.6356 3529.6968,-10791.0073 3580,-10864.5998 3622.4428,-10926.6929 3696.797,-10975.7237 3742.344,-11001.8365"/> +<polygon fill="#000000" stroke="#000000" points="3741.7208,-11003.4953 3746.9323,-11004.4454 3743.4508,-11000.4527 3741.7208,-11003.4953"/> +</g> +<!-- github.com/docker/distribution->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge208" class="edge"> +<title>github.com/docker/distribution->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M2579.7233,-5792.8383C2598.3713,-5899.2993 2692.7886,-6451.3967 2727,-6906.5998 2740.5256,-7086.5655 2727.3138,-9983.5943 2785,-10154.5998 2867.6278,-10399.5422 3002.2963,-10398.2772 3129,-10623.5998 3236.9678,-10815.6036 3322.4999,-11069.7663 3347.2425,-11146.6294"/> +<polygon fill="#000000" stroke="#000000" points="3345.5899,-11147.2071 3348.784,-11151.4333 3348.9226,-11146.1377 3345.5899,-11147.2071"/> +</g> +<!-- github.com/docker/distribution->mime --> +<g id="edge210" class="edge"> +<title>github.com/docker/distribution->mime</title> +<path fill="none" stroke="#000000" d="M2669.3155,-5765.9795C2690.5857,-5759.9738 2711.4781,-5750.2384 2727,-5734.5998 2799.7483,-5661.3046 2734.7763,-5599.8332 2785,-5509.5998 2800.7082,-5481.378 3158.9841,-5105.6725 3187,-5089.5998 3229.761,-5065.068 3286.9277,-5055.5434 3322.2022,-5051.8712"/> +<polygon fill="#000000" stroke="#000000" points="3322.4409,-5053.6062 3327.2435,-5051.3708 3322.0951,-5050.1233 3322.4409,-5053.6062"/> +</g> +<!-- github.com/docker/distribution->net/http --> +<g id="edge211" class="edge"> +<title>github.com/docker/distribution->net/http</title> +<path fill="none" stroke="#000000" d="M2605.2093,-5756.4744C2641.3998,-5731.7454 2701.9105,-5683.6817 2727,-5624.5998 2805.2498,-5440.3336 2731.843,-2214.6061 2785,-2021.5998 2872.3069,-1704.5998 3388.9596,-1039.2853 3522,-738.5998 3556.0085,-661.7369 3512.0621,-608.0865 3580,-558.5998 3729.3753,-449.7934 3972.6862,-530.4451 4061.858,-566.0179"/> +<polygon fill="#000000" stroke="#000000" points="4061.4567,-567.7427 4066.7486,-567.9856 4062.7632,-564.4957 4061.4567,-567.7427"/> +</g> +<!-- github.com/docker/distribution->github.com/docker/distribution/reference --> +<g id="edge206" class="edge"> +<title>github.com/docker/distribution->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M2669.0961,-5768.12C2691.0244,-5762.1513 2712.286,-5751.8983 2727,-5734.5998 2811.7846,-5634.9227 2698.667,-5246.9388 2785,-5148.5998 2797.572,-5134.2795 2814.1786,-5124.5551 2832.1195,-5118.0389"/> +<polygon fill="#000000" stroke="#000000" points="2833.0552,-5119.5676 2837.2129,-5116.285 2831.9155,-5116.2584 2833.0552,-5119.5676"/> +</g> +<!-- github.com/docker/distribution/reference->errors --> +<g id="edge220" class="edge"> +<title>github.com/docker/distribution/reference->errors</title> +<path fill="none" stroke="#000000" d="M2963.1631,-5126.9211C2983.8622,-5185.2406 3056.9943,-5367.1485 3187,-5442.5998 3494.0618,-5620.809 3731.3848,-5259.1298 3983,-5509.5998 4075.1683,-5601.3486 4093.2939,-6592.7803 4095.692,-6759.0154"/> +<polygon fill="#000000" stroke="#000000" points="4093.9439,-6759.162 4095.7647,-6764.1367 4097.4435,-6759.1123 4093.9439,-6759.162"/> +</g> +<!-- github.com/docker/distribution/reference->fmt --> +<g id="edge221" class="edge"> +<title>github.com/docker/distribution/reference->fmt</title> +<path fill="none" stroke="#000000" d="M2960.7555,-5126.898C2981.8009,-5230.2843 3085.6952,-5751.9987 3129,-6183.5998 3136.7826,-6261.1659 3139.7299,-7523.6111 3187,-7585.5998 3281.7755,-7709.8859 3412.5152,-7581.0538 3522,-7692.5998 3585.0386,-7756.8252 3511.1357,-7828.6648 3580,-7886.5998 3718.0278,-8002.7215 3854.149,-7808.3726 3983,-7934.5998 4040.8177,-7991.2402 4085.5656,-8591.5074 4094.4074,-8718.2024"/> +<polygon fill="#000000" stroke="#000000" points="4092.6854,-8718.6688 4094.7778,-8723.5355 4096.177,-8718.4262 4092.6854,-8718.6688"/> +</g> +<!-- github.com/docker/distribution/reference->strings --> +<g id="edge226" class="edge"> +<title>github.com/docker/distribution/reference->strings</title> +<path fill="none" stroke="#000000" d="M2968.1783,-5126.6212C2998.393,-5176.0712 3082.5365,-5318.781 3129,-5447.5998 3168.5824,-5557.3409 3115.1996,-5611.651 3187,-5703.5998 3430.9274,-6015.9776 3761.7309,-5776.783 3983,-6105.5998 4071.3791,-6236.9356 4018.2495,-6658.9398 4041,-6815.5998 4053.9276,-6904.6191 4078.7443,-7008.788 4090.0843,-7054.3192"/> +<polygon fill="#000000" stroke="#000000" points="4088.4235,-7054.8917 4091.3339,-7059.318 4091.819,-7054.0428 4088.4235,-7054.8917"/> +</g> +<!-- github.com/docker/distribution/reference->github.com/opencontainers/go-digest --> +<g id="edge223" class="edge"> +<title>github.com/docker/distribution/reference->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M2960.9389,-5126.8808C2982.9821,-5230.1737 3091.4006,-5751.4643 3129,-6183.5998 3138.3578,-6291.15 3116.9835,-9987.4275 3187,-10069.5998 3285.0499,-10184.6725 3419.5677,-10017.4102 3522,-10128.5998 3616.6584,-10231.351 3529.6699,-10624.2737 3580,-10754.5998 3620.4081,-10859.2336 3714.1414,-10958.184 3758.0349,-11000.7296"/> +<polygon fill="#000000" stroke="#000000" points="3757.0557,-11002.2163 3761.8702,-11004.4262 3759.4847,-10999.6963 3757.0557,-11002.2163"/> +</g> +<!-- github.com/docker/distribution/reference->path --> +<g id="edge224" class="edge"> +<title>github.com/docker/distribution/reference->path</title> +<path fill="none" stroke="#000000" d="M2965.265,-5090.5182C2994.4989,-5025.5126 3093.1749,-4796.9035 3129,-4596.5998 3204.9745,-4171.8146 3083.6823,-3074.5748 3187,-2655.5998 3282.4194,-2268.6544 3242.0335,-2060.8112 3580,-1849.5998 3655.9447,-1802.1383 3915.9913,-1790.1858 3983,-1849.5998 4063.5575,-1921.0271 4027.4322,-2223.795 4041,-2330.5998 4059.8404,-2478.9108 4084.2079,-2657.0045 4092.8173,-2719.5418"/> +<polygon fill="#000000" stroke="#000000" points="4091.0896,-2719.8244 4093.5056,-2724.5389 4094.5569,-2719.3468 4091.0896,-2719.8244"/> +</g> +<!-- github.com/docker/distribution/reference->regexp --> +<g id="edge225" class="edge"> +<title>github.com/docker/distribution/reference->regexp</title> +<path fill="none" stroke="#000000" d="M2965.3241,-5090.5287C2994.7573,-5025.5583 3094.0231,-4797.0533 3129,-4596.5998 3154.8756,-4448.3059 3091.3668,-2003.8536 3187,-1887.5998 3300.0298,-1750.1983 3843.9201,-1690.6418 3983,-1801.5998 4058.8974,-1862.1509 4087.778,-2184.5674 4094.4182,-2274.3682"/> +<polygon fill="#000000" stroke="#000000" points="4092.68,-2274.5944 4094.7888,-2279.4539 4096.1707,-2274.34 4092.68,-2274.5944"/> +</g> +<!-- github.com/docker/distribution/digestset --> +<g id="node91" class="node"> +<title>github.com/docker/distribution/digestset</title> +<g id="a_node91"><a xlink:href="https://godoc.org/github.com/docker/distribution/digestset" xlink:title="github.com/docker/distribution/digestset" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3460.5,-5427.5998C3460.5,-5427.5998 3248.5,-5427.5998 3248.5,-5427.5998 3242.5,-5427.5998 3236.5,-5421.5998 3236.5,-5415.5998 3236.5,-5415.5998 3236.5,-5403.5998 3236.5,-5403.5998 3236.5,-5397.5998 3242.5,-5391.5998 3248.5,-5391.5998 3248.5,-5391.5998 3460.5,-5391.5998 3460.5,-5391.5998 3466.5,-5391.5998 3472.5,-5397.5998 3472.5,-5403.5998 3472.5,-5403.5998 3472.5,-5415.5998 3472.5,-5415.5998 3472.5,-5421.5998 3466.5,-5427.5998 3460.5,-5427.5998"/> +<text text-anchor="middle" x="3354.5" y="-5405.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/digestset</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/reference->github.com/docker/distribution/digestset --> +<g id="edge222" class="edge"> +<title>github.com/docker/distribution/reference->github.com/docker/distribution/digestset</title> +<path fill="none" stroke="#000000" d="M2980.9702,-5126.7509C3050.4201,-5179.3406 3251.5592,-5331.6497 3326.1911,-5388.1634"/> +<polygon fill="#000000" stroke="#000000" points="3325.4559,-5389.8018 3330.4985,-5391.4251 3327.5689,-5387.0115 3325.4559,-5389.8018"/> +</g> +<!-- github.com/docker/distribution/digestset->errors --> +<g id="edge214" class="edge"> +<title>github.com/docker/distribution/digestset->errors</title> +<path fill="none" stroke="#000000" d="M3463.5414,-5391.5501C3483.5992,-5386.0235 3503.9012,-5378.8604 3522,-5369.5998 3552.2983,-5354.0972 3548.0967,-5331.453 3580,-5319.5998 3747.8974,-5257.22 3851.5631,-5197.9235 3983,-5319.5998 4038.2748,-5370.7699 4087.8778,-6576.434 4095.0988,-6759.416"/> +<polygon fill="#000000" stroke="#000000" points="4093.3547,-6759.6019 4095.3,-6764.5292 4096.852,-6759.4642 4093.3547,-6759.6019"/> +</g> +<!-- github.com/docker/distribution/digestset->sort --> +<g id="edge216" class="edge"> +<title>github.com/docker/distribution/digestset->sort</title> +<path fill="none" stroke="#000000" d="M3412.1778,-5391.5947C3450.2963,-5376.4431 3497.5791,-5350.7027 3522,-5310.5998 3580.8678,-5213.9297 3497.7202,-4371.3209 3580,-4293.5998 3645.1032,-4232.1037 3906.9002,-4246.3873 3983,-4293.5998 4032.3184,-4324.1971 4001.3521,-4368.2142 4041,-4410.5998 4047.5585,-4417.6112 4056.0026,-4423.55 4064.2961,-4428.3364"/> +<polygon fill="#000000" stroke="#000000" points="4063.654,-4429.9814 4068.8752,-4430.8765 4065.3518,-4426.9207 4063.654,-4429.9814"/> +</g> +<!-- github.com/docker/distribution/digestset->strings --> +<g id="edge217" class="edge"> +<title>github.com/docker/distribution/digestset->strings</title> +<path fill="none" stroke="#000000" d="M3441.7659,-5427.6156C3583.2511,-5461.9468 3859.3103,-5551.3467 3983,-5745.5998 4046.9495,-5846.0317 4025.5021,-6697.5494 4041,-6815.5998 4052.7088,-6904.7876 4078.1495,-7008.8702 4089.8666,-7054.3493"/> +<polygon fill="#000000" stroke="#000000" points="4088.2118,-7054.9401 4091.1586,-7059.3422 4091.6002,-7054.0632 4088.2118,-7054.9401"/> +</g> +<!-- github.com/docker/distribution/digestset->sync --> +<g id="edge218" class="edge"> +<title>github.com/docker/distribution/digestset->sync</title> +<path fill="none" stroke="#000000" d="M3367.655,-5391.2616C3401.1305,-5343.3837 3489.213,-5209.5119 3522,-5082.5998 3546.9984,-4985.8359 3514.8893,-3360.4205 3580,-3284.5998 3704.5368,-3139.5781 3971.4744,-3167.7711 4063.7968,-3182.677"/> +<polygon fill="#000000" stroke="#000000" points="4063.6259,-3184.4224 4068.8437,-3183.5069 4064.1939,-3180.9687 4063.6259,-3184.4224"/> +</g> +<!-- github.com/docker/distribution/digestset->github.com/opencontainers/go-digest --> +<g id="edge215" class="edge"> +<title>github.com/docker/distribution/digestset->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M3358.2706,-5427.9056C3379.602,-5532.4579 3485.4896,-6064.8705 3522,-6505.5998 3541.4901,-6740.8715 3503.6857,-10531.1972 3580,-10754.5998 3616.2399,-10860.6886 3711.9907,-10958.8301 3757.204,-11000.9419"/> +<polygon fill="#000000" stroke="#000000" points="3756.0372,-11002.2465 3760.8949,-11004.3596 3758.4152,-10999.6783 3756.0372,-11002.2465"/> +</g> +<!-- github.com/docker/distribution/metrics --> +<g id="node92" class="node"> +<title>github.com/docker/distribution/metrics</title> +<g id="a_node92"><a xlink:href="https://godoc.org/github.com/docker/distribution/metrics" xlink:title="github.com/docker/distribution/metrics" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1629,-6018.5998C1629,-6018.5998 1423,-6018.5998 1423,-6018.5998 1417,-6018.5998 1411,-6012.5998 1411,-6006.5998 1411,-6006.5998 1411,-5994.5998 1411,-5994.5998 1411,-5988.5998 1417,-5982.5998 1423,-5982.5998 1423,-5982.5998 1629,-5982.5998 1629,-5982.5998 1635,-5982.5998 1641,-5988.5998 1641,-5994.5998 1641,-5994.5998 1641,-6006.5998 1641,-6006.5998 1641,-6012.5998 1635,-6018.5998 1629,-6018.5998"/> +<text text-anchor="middle" x="1526" y="-5996.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/metrics</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics --> +<g id="node93" class="node"> +<title>github.com/docker/go-metrics</title> +<g id="a_node93"><a xlink:href="https://godoc.org/github.com/docker/go-metrics" xlink:title="github.com/docker/go-metrics" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1927.5,-6499.5998C1927.5,-6499.5998 1769.5,-6499.5998 1769.5,-6499.5998 1763.5,-6499.5998 1757.5,-6493.5998 1757.5,-6487.5998 1757.5,-6487.5998 1757.5,-6475.5998 1757.5,-6475.5998 1757.5,-6469.5998 1763.5,-6463.5998 1769.5,-6463.5998 1769.5,-6463.5998 1927.5,-6463.5998 1927.5,-6463.5998 1933.5,-6463.5998 1939.5,-6469.5998 1939.5,-6475.5998 1939.5,-6475.5998 1939.5,-6487.5998 1939.5,-6487.5998 1939.5,-6493.5998 1933.5,-6499.5998 1927.5,-6499.5998"/> +<text text-anchor="middle" x="1848.5" y="-6477.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go-metrics</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/metrics->github.com/docker/go-metrics --> +<g id="edge219" class="edge"> +<title>github.com/docker/distribution/metrics->github.com/docker/go-metrics</title> +<path fill="none" stroke="#000000" d="M1538.2486,-6018.8682C1588.4918,-6093.8047 1778.7204,-6377.5254 1833.5972,-6459.3727"/> +<polygon fill="#000000" stroke="#000000" points="1832.1733,-6460.3915 1836.4113,-6463.5699 1835.0804,-6458.4423 1832.1733,-6460.3915"/> +</g> +<!-- github.com/docker/go-metrics->fmt --> +<g id="edge371" class="edge"> +<title>github.com/docker/go-metrics->fmt</title> +<path fill="none" stroke="#000000" d="M1855.5192,-6499.7094C1898.2538,-6608.7657 2131.4377,-7187.035 2426,-7594.5998 2681.0548,-7947.5009 2787.1672,-8039.1879 3187,-8211.5998 3328.4622,-8272.5997 3385.7532,-8228.7015 3522,-8300.5998 3552.1001,-8316.4839 3549.3029,-8335.9026 3580,-8350.5998 3744.2504,-8429.2399 3844.3046,-8306.5911 3983,-8424.5998 4029.7303,-8464.3602 4075.843,-8652.2507 4090.8976,-8718.4501"/> +<polygon fill="#000000" stroke="#000000" points="4089.2007,-8718.8806 4092.0104,-8723.3715 4092.6145,-8718.1086 4089.2007,-8718.8806"/> +</g> +<!-- github.com/docker/go-metrics->sync --> +<g id="edge375" class="edge"> +<title>github.com/docker/go-metrics->sync</title> +<path fill="none" stroke="#000000" d="M1851.0133,-6463.4739C1880.4719,-6250.8478 2162.6845,-4209.3247 2368,-2555.5998 2381.7815,-2444.5959 2343.4119,-2131.0386 2426,-2055.5998 2553.7333,-1938.9237 3856.6396,-1934.4383 3983,-2052.5998 4041.8644,-2107.6448 4032.4556,-2695.4626 4041,-2775.5998 4056.8884,-2924.6145 4083.0819,-3102.9279 4092.4952,-3165.5234"/> +<polygon fill="#000000" stroke="#000000" points="4090.7732,-3165.8415 4093.2487,-3170.5249 4094.2342,-3165.32 4090.7732,-3165.8415"/> +</g> +<!-- github.com/docker/go-metrics->time --> +<g id="edge376" class="edge"> +<title>github.com/docker/go-metrics->time</title> +<path fill="none" stroke="#000000" d="M1852.5817,-6499.6465C1871.3354,-6582.3956 1951.382,-6933.7844 2023,-7220.5998 2168.86,-7804.7391 2241.8452,-7941.8904 2368,-8530.5998 2400.9467,-8684.3479 2328.9381,-8755.8947 2426,-8879.5998 2515.8441,-8994.1059 2644.1438,-8888.9401 2727,-9008.5998 2807.9208,-9125.4645 2699.9341,-10172.7169 2785,-10286.5998 2881.8146,-10416.2115 3010.3726,-10295.6008 3129,-10405.5998 3176.5856,-10449.7243 3140.4166,-10494.4186 3187,-10539.5998 3301.9915,-10651.13 3417.583,-10551.1127 3522,-10672.5998 3596.6299,-10759.4302 3496.5558,-10845.2021 3580,-10923.5998 3712.3282,-11047.925 3855.0164,-10861.8066 3983,-10990.5998 4098.1621,-11106.4904 3948.5943,-11218.8626 4041,-11353.5998 4046.6936,-11361.9017 4055.3027,-11368.348 4064.0302,-11373.2077"/> +<polygon fill="#000000" stroke="#000000" points="4063.6323,-11374.9749 4068.8733,-11375.7461 4065.2571,-11371.8749 4063.6323,-11374.9749"/> +</g> +<!-- github.com/docker/go-metrics->net/http --> +<g id="edge374" class="edge"> +<title>github.com/docker/go-metrics->net/http</title> +<path fill="none" stroke="#000000" d="M1848.6874,-6463.592C1851.399,-6211.7224 1884.687,-3389.9018 2023,-2570.5998 2127.3037,-1952.7527 2169.5374,-1791.3002 2426,-1219.5998 2632.5925,-759.0687 2707.5642,-534.4311 3187,-376.5998 3523.1775,-265.9295 3682.0972,-167.2648 3983,-353.5998 4056.8484,-399.3307 4083.9876,-509.4166 4092.5499,-557.4887"/> +<polygon fill="#000000" stroke="#000000" points="4090.8382,-557.8609 4093.4132,-562.4904 4094.2872,-557.2656 4090.8382,-557.8609"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus --> +<g id="node107" class="node"> +<title>github.com/prometheus/client_golang/prometheus</title> +<g id="a_node107"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus" xlink:title="github.com/prometheus/client_golang/prometheus" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2710,-6956.5998C2710,-6956.5998 2443,-6956.5998 2443,-6956.5998 2437,-6956.5998 2431,-6950.5998 2431,-6944.5998 2431,-6944.5998 2431,-6932.5998 2431,-6932.5998 2431,-6926.5998 2437,-6920.5998 2443,-6920.5998 2443,-6920.5998 2710,-6920.5998 2710,-6920.5998 2716,-6920.5998 2722,-6926.5998 2722,-6932.5998 2722,-6932.5998 2722,-6944.5998 2722,-6944.5998 2722,-6950.5998 2716,-6956.5998 2710,-6956.5998"/> +<text text-anchor="middle" x="2576.5" y="-6934.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus --> +<g id="edge372" class="edge"> +<title>github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus</title> +<path fill="none" stroke="#000000" d="M1877.4877,-6499.7968C1993.0219,-6572.3231 2420.3372,-6840.5691 2543.2368,-6917.7189"/> +<polygon fill="#000000" stroke="#000000" points="2542.6602,-6919.4232 2547.8254,-6920.5994 2544.5211,-6916.4589 2542.6602,-6919.4232"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp --> +<g id="node108" class="node"> +<title>github.com/prometheus/client_golang/prometheus/promhttp</title> +<g id="a_node108"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" xlink:title="github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2356,-6499.5998C2356,-6499.5998 2035,-6499.5998 2035,-6499.5998 2029,-6499.5998 2023,-6493.5998 2023,-6487.5998 2023,-6487.5998 2023,-6475.5998 2023,-6475.5998 2023,-6469.5998 2029,-6463.5998 2035,-6463.5998 2035,-6463.5998 2356,-6463.5998 2356,-6463.5998 2362,-6463.5998 2368,-6469.5998 2368,-6475.5998 2368,-6475.5998 2368,-6487.5998 2368,-6487.5998 2368,-6493.5998 2362,-6499.5998 2356,-6499.5998"/> +<text text-anchor="middle" x="2195.5" y="-6477.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/promhttp</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus/promhttp --> +<g id="edge373" class="edge"> +<title>github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus/promhttp</title> +<path fill="none" stroke="#000000" d="M1939.6702,-6481.5998C1963.7722,-6481.5998 1990.6336,-6481.5998 2017.6361,-6481.5998"/> +<polygon fill="#000000" stroke="#000000" points="2017.6791,-6483.3499 2022.6791,-6481.5998 2017.679,-6479.8499 2017.6791,-6483.3499"/> +</g> +<!-- github.com/gorilla/mux->bytes --> +<g id="edge411" class="edge"> +<title>github.com/gorilla/mux->bytes</title> +<path fill="none" stroke="#000000" d="M3785.6013,-2377.6292C3810.4091,-2487.7026 3940.7503,-3080.7476 3983,-3572.5998 4064.5444,-4521.9035 4020.9577,-11195.0111 4041,-12147.5998 4054.1587,-12773.0196 4088.4311,-13539.1827 4094.9283,-13681.3377"/> +<polygon fill="#000000" stroke="#000000" points="4093.1882,-13681.5955 4095.165,-13686.5103 4096.6845,-13681.4355 4093.1882,-13681.5955"/> +</g> +<!-- github.com/gorilla/mux->context --> +<g id="edge412" class="edge"> +<title>github.com/gorilla/mux->context</title> +<path fill="none" stroke="#000000" d="M3792.3236,-2341.3067C3826.8154,-2282.052 3934.7053,-2089.1057 3983,-1914.5998 4009.3841,-1819.2646 4080.1778,-1097.8593 4093.7374,-958.032"/> +<polygon fill="#000000" stroke="#000000" points="4095.4914,-958.0746 4094.2319,-952.9291 4092.0077,-957.737 4095.4914,-958.0746"/> +</g> +<!-- github.com/gorilla/mux->errors --> +<g id="edge413" class="edge"> +<title>github.com/gorilla/mux->errors</title> +<path fill="none" stroke="#000000" d="M3785.4274,-2377.6452C3809.2099,-2487.8133 3934.5481,-3081.3198 3983,-3572.5998 4062.082,-4374.4554 4012.2734,-4579.3663 4041,-5384.5998 4060.9014,-5942.4553 4089.3952,-6625.3544 4095.016,-6759.2216"/> +<polygon fill="#000000" stroke="#000000" points="4093.2781,-6759.548 4095.2365,-6764.4701 4096.775,-6759.401 4093.2781,-6759.548"/> +</g> +<!-- github.com/gorilla/mux->fmt --> +<g id="edge414" class="edge"> +<title>github.com/gorilla/mux->fmt</title> +<path fill="none" stroke="#000000" d="M3785.5578,-2377.633C3810.109,-2487.7289 3939.1979,-3080.8834 3983,-3572.5998 4070.4545,-4554.3521 3997.7103,-7022.9111 4041,-8007.5998 4053.3264,-8287.983 4084.9835,-8627.5102 4093.7309,-8718.3362"/> +<polygon fill="#000000" stroke="#000000" points="4092.0036,-8718.6568 4094.226,-8723.4655 4095.4874,-8718.3205 4092.0036,-8718.6568"/> +</g> +<!-- github.com/gorilla/mux->strconv --> +<g id="edge419" class="edge"> +<title>github.com/gorilla/mux->strconv</title> +<path fill="none" stroke="#000000" d="M3785.378,-2377.6502C3808.8693,-2487.8475 3932.7869,-3081.4967 3983,-3572.5998 4049.9574,-4227.4684 3968.3809,-4398.3349 4041,-5052.5998 4052.5399,-5156.5691 4079.1772,-5278.7668 4090.6186,-5328.6518"/> +<polygon fill="#000000" stroke="#000000" points="4088.9149,-5329.0522 4091.7424,-5333.5319 4092.3256,-5328.2667 4088.9149,-5329.0522"/> +</g> +<!-- github.com/gorilla/mux->strings --> +<g id="edge420" class="edge"> +<title>github.com/gorilla/mux->strings</title> +<path fill="none" stroke="#000000" d="M3785.5247,-2377.636C3809.8805,-2487.7495 3938.0165,-3080.9901 3983,-3572.5998 4048.6789,-4290.3831 3960.1539,-6099.3663 4041,-6815.5998 4051.0895,-6904.9853 4077.3594,-7008.9666 4089.5775,-7054.3846"/> +<polygon fill="#000000" stroke="#000000" points="4087.9312,-7055.0009 4090.9258,-7059.3707 4091.3098,-7054.0872 4087.9312,-7055.0009"/> +</g> +<!-- github.com/gorilla/mux->net/http --> +<g id="edge415" class="edge"> +<title>github.com/gorilla/mux->net/http</title> +<path fill="none" stroke="#000000" d="M3792.7404,-2341.4156C3828.489,-2282.4894 3939.746,-2090.4229 3983,-1914.5998 4090.622,-1477.1275 3990.105,-1350.2316 4041,-902.5998 4053.8218,-789.8296 4080.2088,-656.5919 4091.1162,-603.8044"/> +<polygon fill="#000000" stroke="#000000" points="4092.8823,-603.9056 4092.184,-598.6544 4089.4552,-603.1949 4092.8823,-603.9056"/> +</g> +<!-- github.com/gorilla/mux->net/url --> +<g id="edge416" class="edge"> +<title>github.com/gorilla/mux->net/url</title> +<path fill="none" stroke="#000000" d="M3790.9166,-2341.345C3821.572,-2281.4289 3920.1536,-2084.8606 3983,-1914.5998 4033.3351,-1778.234 4075.8966,-1608.5165 4090.5501,-1547.6497"/> +<polygon fill="#000000" stroke="#000000" points="4092.2542,-1548.0479 4091.7193,-1542.7776 4088.8508,-1547.2311 4092.2542,-1548.0479"/> +</g> +<!-- github.com/gorilla/mux->path --> +<g id="edge417" class="edge"> +<title>github.com/gorilla/mux->path</title> +<path fill="none" stroke="#000000" d="M3854.6512,-2356.8052C3896.8542,-2359.0445 3948.2731,-2368.8795 3983,-2399.5998 4032.6574,-2443.5281 4077.3897,-2648.9715 4091.4434,-2719.083"/> +<polygon fill="#000000" stroke="#000000" points="4089.7858,-2719.72 4092.4789,-2724.2819 4093.2184,-2719.0363 4089.7858,-2719.72"/> +</g> +<!-- github.com/gorilla/mux->regexp --> +<g id="edge418" class="edge"> +<title>github.com/gorilla/mux->regexp</title> +<path fill="none" stroke="#000000" d="M3854.8394,-2345.1418C3920.8623,-2332.1262 4014.7947,-2313.6085 4063.6569,-2303.9759"/> +<polygon fill="#000000" stroke="#000000" points="4064.248,-2305.6431 4068.8151,-2302.959 4063.571,-2302.2092 4064.248,-2305.6431"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->fmt --> +<g id="edge264" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->fmt</title> +<path fill="none" stroke="#000000" d="M3793.0834,-1899.7489C3829.8665,-1958.5781 3943.8945,-2150.4619 3983,-2327.5998 4051.0285,-2635.7521 4027.5918,-7692.3128 4041,-8007.5998 4052.9246,-8288.0003 4084.8693,-8627.5152 4093.7065,-8718.3372"/> +<polygon fill="#000000" stroke="#000000" points="4091.9796,-8718.6599 4094.2068,-8723.4664 4095.4631,-8718.3201 4091.9796,-8718.6599"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->strings --> +<g id="edge267" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->strings</title> +<path fill="none" stroke="#000000" d="M3793.0635,-1899.7533C3829.7867,-1958.5958 3943.6544,-2150.5151 3983,-2327.5998 4091.1678,-2814.4362 3986.2919,-6319.9013 4041,-6815.5998 4050.8678,-6905.01 4077.2512,-7008.9787 4089.5379,-7054.3891"/> +<polygon fill="#000000" stroke="#000000" points="4087.8928,-7055.0089 4090.8939,-7059.3742 4091.2701,-7054.0902 4087.8928,-7055.0089"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->sync --> +<g id="edge268" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->sync</title> +<path fill="none" stroke="#000000" d="M3955.2151,-1899.1271C3965.5571,-1905.1011 3974.9921,-1912.4939 3983,-1921.5998 4045.8069,-1993.0192 4031.4954,-2680.9685 4041,-2775.5998 4055.9762,-2924.709 4082.7339,-3102.964 4092.3956,-3165.5337"/> +<polygon fill="#000000" stroke="#000000" points="4090.6751,-3165.8596 4093.1693,-3170.5332 4094.134,-3165.3243 4090.6751,-3165.8596"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->net/http --> +<g id="edge265" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->net/http</title> +<path fill="none" stroke="#000000" d="M3785.9459,-1863.2085C3820.9176,-1718.5402 4051.4116,-765.0498 4090.4019,-603.7574"/> +<polygon fill="#000000" stroke="#000000" points="4092.13,-604.0566 4091.6039,-598.7854 4088.7279,-603.2342 4092.13,-604.0566"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->net/url --> +<g id="edge266" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->net/url</title> +<path fill="none" stroke="#000000" d="M3799.191,-1863.4173C3834.8096,-1826.6217 3917.3697,-1740.3824 3983,-1664.5998 4018.2764,-1623.8665 4057.3244,-1574.455 4079.0407,-1546.5596"/> +<polygon fill="#000000" stroke="#000000" points="4080.429,-1547.6251 4082.1165,-1542.6036 4077.6658,-1545.4767 4080.429,-1547.6251"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->errors --> +<g id="edge269" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->errors</title> +<path fill="none" stroke="#000000" d="M3808.5422,-4702.7569C3854.1096,-4735.1095 3944.9386,-4807.5674 3983,-4894.5998 4022.2668,-4984.3886 4086.5805,-6548.9387 4095.0656,-6759.282"/> +<polygon fill="#000000" stroke="#000000" points="4093.3282,-6759.6305 4095.2781,-6764.5559 4096.8253,-6759.4895 4093.3282,-6759.6305"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->fmt --> +<g id="edge270" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->fmt</title> +<path fill="none" stroke="#000000" d="M3809.292,-4702.8521C3855.5524,-4735.0869 3946.8338,-4807.0363 3983,-4894.5998 4049.0327,-5054.4743 4032.9232,-7834.814 4041,-8007.5998 4054.1048,-8287.9477 4085.2047,-8627.5002 4093.778,-8718.334"/> +<polygon fill="#000000" stroke="#000000" points="4092.0501,-8718.6509 4094.2632,-8723.4638 4095.5345,-8718.3212 4092.0501,-8718.6509"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->io --> +<g id="edge271" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->io</title> +<path fill="none" stroke="#000000" d="M3809.4057,-4702.8055C3855.8356,-4734.9709 3947.3629,-4806.8196 3983,-4894.5998 4075.4023,-5122.2023 3995.6451,-13496.179 4041,-13737.5998 4049.4077,-13782.3532 4070.9371,-13831.2316 4084.4305,-13858.9799"/> +<polygon fill="#000000" stroke="#000000" points="4082.9135,-13859.8612 4086.6875,-13863.5787 4086.0555,-13858.3191 4082.9135,-13859.8612"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->strconv --> +<g id="edge274" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->strconv</title> +<path fill="none" stroke="#000000" d="M3854.473,-4702.7349C3898.2579,-4717.381 3951.4114,-4742.4967 3983,-4783.5998 3991.595,-4794.7837 4071.9897,-5223.0212 4091.6685,-5328.3695"/> +<polygon fill="#000000" stroke="#000000" points="4089.9745,-5328.8315 4092.6126,-5333.4253 4093.415,-5328.189 4089.9745,-5328.8315"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->sync --> +<g id="edge275" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->sync</title> +<path fill="none" stroke="#000000" d="M3823.558,-4666.5418C3871.3799,-4643.5368 3947.4175,-4598.8957 3983,-4534.5998 4061.0469,-4393.5729 4027.4682,-3972.2136 4041,-3811.5998 4060.7351,-3577.3569 4086.4262,-3293.7435 4093.885,-3211.7924"/> +<polygon fill="#000000" stroke="#000000" points="4095.6391,-3211.8268 4094.3497,-3206.6887 4092.1535,-3211.5094 4095.6391,-3211.8268"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->net/http --> +<g id="edge272" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->net/http</title> +<path fill="none" stroke="#000000" d="M3825.153,-4666.5244C3873.7092,-4643.767 3949.6943,-4599.6144 3983,-4534.5998 4075.0094,-4354.9921 4022.3455,-1103.5393 4041,-902.5998 4051.4915,-789.589 4079.1916,-656.4869 4090.7832,-603.77"/> +<polygon fill="#000000" stroke="#000000" points="4092.5495,-603.8868 4091.9191,-598.627 4089.1318,-603.1319 4092.5495,-603.8868"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->regexp --> +<g id="edge273" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->regexp</title> +<path fill="none" stroke="#000000" d="M3824.969,-4666.4292C3873.3544,-4643.5834 3949.1811,-4599.349 3983,-4534.5998 4029.9371,-4444.7347 4032.6867,-2811.643 4041,-2710.5998 4053.2882,-2561.2451 4081.7086,-2383.142 4092.1023,-2320.6391"/> +<polygon fill="#000000" stroke="#000000" points="4093.8387,-2320.8649 4092.9354,-2315.6451 4090.3864,-2320.289 4093.8387,-2320.8649"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->context --> +<g id="edge276" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->context</title> +<path fill="none" stroke="#000000" d="M1178.5166,-5388.4093C1207.5222,-5345.4665 1279.9125,-5233.6417 1320,-5130.5998 1381.3006,-4973.0313 1918.3749,-2302.4124 2023,-2169.5998 2129.3943,-2034.5415 2270.8707,-2136.4673 2368,-1994.5998 2514.8524,-1780.1067 2262.4065,-1614.6149 2426,-1412.5998 2514.3689,-1303.4766 2626.8075,-1414.9787 2727,-1316.5998 2784.2631,-1260.3732 2734.3787,-1207.8733 2785,-1145.5998 2915.8256,-984.6603 2991.4411,-977.6912 3187,-908.5998 3353.4742,-849.7842 3403.915,-863.5234 3580,-850.5998 3758.6306,-837.4894 3812.96,-794.3221 3983,-850.5998 4019.9213,-862.8196 4054.5954,-892.2942 4075.5553,-912.8571"/> +<polygon fill="#000000" stroke="#000000" points="4074.5271,-914.3026 4079.3068,-916.5868 4076.9948,-911.8205 4074.5271,-914.3026"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->fmt --> +<g id="edge277" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->fmt</title> +<path fill="none" stroke="#000000" d="M1170.8908,-5424.7273C1191.6354,-5500.6696 1275.9868,-5800.1029 1378,-6033.5998 1742.1732,-6867.1516 1820.8131,-7112.4979 2426,-7791.5998 2568.4007,-7951.3928 2603.6025,-7998.9922 2785,-8112.5998 3104.6665,-8312.8041 3214.8054,-8315.2528 3580,-8409.5998 3756.3167,-8455.1508 3840.2378,-8370.5448 3983,-8483.5998 4059.5331,-8544.2073 4085.4205,-8667.1943 4093.1093,-8718.4064"/> +<polygon fill="#000000" stroke="#000000" points="4091.3874,-8718.727 4093.8384,-8723.4234 4094.851,-8718.2236 4091.3874,-8718.727"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/opencontainers/go-digest --> +<g id="edge280" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M1166.3081,-5424.601C1170.2624,-5647.4906 1213.7067,-7869.2701 1378,-8102.5998 1461.034,-8220.525 1550.1266,-8161.7327 1674,-8235.5998 2007.5691,-8434.5109 2148.5022,-8446.2021 2368,-8766.5998 2420.0662,-8842.6 2362.2778,-8899.0686 2426,-8965.5998 2523.7023,-9067.609 2642.1565,-8954.6698 2727,-9067.5998 2813.8485,-9183.1985 2695.1821,-10254.2928 2785,-10367.5998 2883.6778,-10492.0838 3004.3369,-10366.1483 3129,-10464.5998 3170.534,-10497.4009 3148.5459,-10532.2368 3187,-10568.5998 3307.3069,-10682.3643 3418.6217,-10602.2591 3522,-10731.5998 3592.4037,-10819.6848 3499.6425,-10899.4903 3580,-10978.5998 3603.2521,-11001.4908 3635.4818,-11013.6604 3667.3517,-11019.8409"/> +<polygon fill="#000000" stroke="#000000" points="3667.2278,-11021.5975 3672.4622,-11020.7822 3667.8619,-11018.1554 3667.2278,-11021.5975"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution --> +<g id="edge278" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M1235.4658,-5424.7235C1473.7809,-5486.9 2256.9607,-5691.2319 2502.3163,-5755.2453"/> +<polygon fill="#000000" stroke="#000000" points="2502.0259,-5756.978 2507.3058,-5756.547 2502.9096,-5753.5914 2502.0259,-5756.978"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution/metrics --> +<g id="edge279" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution/metrics</title> +<path fill="none" stroke="#000000" d="M1176.9707,-5424.7014C1229.4012,-5511.2117 1454.2901,-5882.2785 1512.3032,-5978"/> +<polygon fill="#000000" stroke="#000000" points="1510.85,-5978.9789 1514.9382,-5982.3478 1513.8432,-5977.1648 1510.85,-5978.9789"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->context --> +<g id="edge281" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->context</title> +<path fill="none" stroke="#000000" d="M779.0747,-5208.48C829.9121,-4985.356 1357.5931,-2751.9221 2426,-1313.5998 2566.8729,-1123.9523 2578.4602,-1036.2848 2785,-921.5998 3252.3499,-662.0952 3492.2549,-602.6379 3983,-814.5998 4028.8833,-834.4177 4064.7565,-883.05 4082.9212,-911.9548"/> +<polygon fill="#000000" stroke="#000000" points="4081.5636,-913.0867 4085.6833,-916.4171 4084.5396,-911.2446 4081.5636,-913.0867"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->sync --> +<g id="edge286" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->sync</title> +<path fill="none" stroke="#000000" d="M879.621,-5208.586C1019.2309,-5182.6247 1256.2959,-5131.2067 1320,-5071.5998 2385.7178,-4074.4242 1256.6283,-2852.9172 2426,-1979.5998 2495.3069,-1927.8396 3920.3844,-1909.9186 3983,-1969.5998 4047.9938,-2031.5477 4031.8473,-2686.2804 4041,-2775.5998 4056.2763,-2924.6785 4082.8484,-3102.9523 4092.4284,-3165.5304"/> +<polygon fill="#000000" stroke="#000000" points="4090.7074,-3165.8537 4093.1954,-3170.5305 4094.1669,-3165.3229 4090.7074,-3165.8537"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/opencontainers/go-digest --> +<g id="edge285" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M776.7697,-5244.6463C798.7518,-5466.0653 1024.1747,-7659.6716 1378,-8190.5998 1552.7996,-8452.8932 1762.8593,-8346.749 1965,-8588.5998 2001.0319,-8631.7101 2741.9973,-10401.4396 2785,-10437.5998 2905.6183,-10539.0256 3010.6382,-10419.5497 3129,-10523.5998 3182.0609,-10570.2448 3136.2468,-10622.4539 3187,-10671.5998 3300.5083,-10781.5134 3412.6495,-10676.5489 3522,-10790.5998 3582.5162,-10853.7173 3515.6202,-10919.4283 3580,-10978.5998 3603.7733,-11000.4499 3635.8003,-11012.3661 3667.3166,-11018.6357"/> +<polygon fill="#000000" stroke="#000000" points="3667.1305,-11020.3815 3672.369,-11019.5941 3667.7828,-11016.9428 3667.1305,-11020.3815"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution --> +<g id="edge282" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M954.0531,-5226.5998C1106.2058,-5226.5998 1330.4271,-5226.5998 1526,-5226.5998 1526,-5226.5998 1526,-5226.5998 1848.5,-5226.5998 2083.298,-5226.5998 2201.3917,-5157.1556 2368,-5322.5998 2464.9821,-5418.9044 2361.8699,-5503.9042 2426,-5624.5998 2455.1912,-5679.5389 2511.2352,-5727.1901 2546.0842,-5753.3132"/> +<polygon fill="#000000" stroke="#000000" points="2545.1688,-5754.8132 2550.2258,-5756.3912 2547.2566,-5752.0041 2545.1688,-5754.8132"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/reference --> +<g id="edge283" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M954.0256,-5216.9183C1383.669,-5193.6837 2464.7375,-5135.2208 2832.372,-5115.3396"/> +<polygon fill="#000000" stroke="#000000" points="2832.567,-5117.0817 2837.4652,-5115.0641 2832.378,-5113.5868 2832.567,-5117.0817"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/registry/storage/cache --> +<g id="edge284" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/registry/storage/cache</title> +<path fill="none" stroke="#000000" d="M814.2808,-5244.683C887.6158,-5278.4434 1044.8745,-5350.8387 1121.7965,-5386.2504"/> +<polygon fill="#000000" stroke="#000000" points="1121.4717,-5388.0273 1126.7453,-5388.5286 1122.9353,-5384.848 1121.4717,-5388.0273"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->bufio --> +<g id="edge307" class="edge"> +<title>github.com/docker/docker/pkg/idtools->bufio</title> +<path fill="none" stroke="#000000" d="M2200.7678,-15166.6267C2221.123,-15234.147 2299.842,-15476.4896 2426,-15638.5998 2552.8327,-15801.577 2590.5796,-15863.9664 2785,-15933.5998 3093.3734,-16044.0465 3194.9808,-15952.3364 3522,-15933.5998 3624.9504,-15927.7013 3913.2082,-15956.5122 3983,-15880.5998 4126.0231,-15725.0339 3934.0904,-14161.8818 4041,-13979.5998 4046.3255,-13970.5198 4055.2175,-13963.6459 4064.3159,-13958.5869"/> +<polygon fill="#000000" stroke="#000000" points="4065.1663,-13960.1171 4068.7842,-13956.2475 4063.5429,-13957.0163 4065.1663,-13960.1171"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->bytes --> +<g id="edge308" class="edge"> +<title>github.com/docker/docker/pkg/idtools->bytes</title> +<path fill="none" stroke="#000000" d="M2307.5408,-15157.7955C2663.6919,-15184.3153 3759.5543,-15244.1641 3983,-14997.5998 4068.7646,-14902.9617 4014.8117,-13974.6043 4041,-13849.5998 4050.3937,-13804.7608 4071.5984,-13755.4688 4084.7631,-13727.4528"/> +<polygon fill="#000000" stroke="#000000" points="4086.4039,-13728.0769 4086.9633,-13722.8091 4083.2409,-13726.5783 4086.4039,-13728.0769"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->fmt --> +<g id="edge309" class="edge"> +<title>github.com/docker/docker/pkg/idtools->fmt</title> +<path fill="none" stroke="#000000" d="M2196.3983,-15130.3701C2206.9696,-14917.5802 2309.3846,-12902.9242 2426,-12316.5998 2511.16,-11888.4285 2628.2913,-11806.8521 2727,-11381.5998 2747.2091,-11294.5358 2721.6082,-11046.608 2785,-10983.5998 2894.4869,-10874.7755 3025.0881,-11049.7595 3129,-10935.5998 3226.0285,-10829.0024 3100.6382,-9755.0085 3187,-9639.5998 3281.3143,-9513.5639 3420.1876,-9644.6601 3522,-9524.5998 3622.5626,-9406.0133 3468.5008,-9287.9674 3580,-9179.5998 3709.6757,-9053.5663 3845.9381,-9241.5587 3983,-9123.5998 4039.1826,-9075.2476 4080.5049,-8840.0426 4092.4622,-8764.7987"/> +<polygon fill="#000000" stroke="#000000" points="4094.2219,-8764.874 4093.2722,-8759.6624 4090.7647,-8764.3287 4094.2219,-8764.874"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->io --> +<g id="edge312" class="edge"> +<title>github.com/docker/docker/pkg/idtools->io</title> +<path fill="none" stroke="#000000" d="M2233.133,-15166.7416C2277.8784,-15187.2172 2355.4,-15219.2761 2426,-15231.5998 2557.7851,-15254.6039 2593.2294,-15232.9842 2727,-15231.5998 3006.156,-15228.7108 3783.2224,-15400.6006 3983,-15205.5998 4085.7539,-15105.3028 3967.4669,-14037.9316 4041,-13914.5998 4046.3907,-13905.5584 4055.2998,-13898.6945 4064.3937,-13893.633"/> +<polygon fill="#000000" stroke="#000000" points="4065.2432,-13895.1636 4068.8583,-13891.2913 4063.6175,-13892.064 4065.2432,-13895.1636"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->sort --> +<g id="edge317" class="edge"> +<title>github.com/docker/docker/pkg/idtools->sort</title> +<path fill="none" stroke="#000000" d="M2196.5319,-15130.4539C2208.5884,-14917.5945 2323.0493,-12873.8655 2368,-11219.5998 2370.0928,-11142.5824 2375.6879,-5730.9502 2426,-5672.5998 2514.4626,-5570.0036 2634.3511,-5723.4319 2727,-5624.5998 2821.3373,-5523.9667 2759.9109,-5142.2357 2785,-5006.5998 2905.8595,-4353.2127 2657.7361,-3968.3478 3187,-3566.5998 3244.9102,-3522.642 3929.523,-3522.0411 3983,-3572.5998 4118.6433,-3700.8408 3942.4024,-4252.0964 4041,-4410.5998 4046.3665,-4419.2268 4054.9755,-4425.7853 4063.8048,-4430.6481"/> +<polygon fill="#000000" stroke="#000000" points="4063.4671,-4432.4427 4068.7134,-4433.1775 4065.0704,-4429.3315 4063.4671,-4432.4427"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->strconv --> +<g id="edge318" class="edge"> +<title>github.com/docker/docker/pkg/idtools->strconv</title> +<path fill="none" stroke="#000000" d="M2196.531,-15130.4539C2208.5766,-14917.5942 2322.9388,-12873.8625 2368,-11219.5998 2370.0714,-11143.5543 2378.0955,-5801.6961 2426,-5742.5998 2512.3032,-5636.134 2636.2031,-5778.2601 2727,-5675.5998 2815.5986,-5575.425 2695.074,-5175.5847 2785,-5076.5998 2889.7383,-4961.3106 2977.027,-5043.7458 3129,-5009.5998 3330.8265,-4964.2526 3374.7515,-4920.3558 3580,-4894.5998 3757.7173,-4872.2986 3841.9657,-4784.1909 3983,-4894.5998 4018.3743,-4922.2927 4076.3398,-5239.5242 4091.9968,-5328.5159"/> +<polygon fill="#000000" stroke="#000000" points="4090.2937,-5328.9361 4092.8816,-5333.5584 4093.741,-5328.3311 4090.2937,-5328.9361"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->strings --> +<g id="edge319" class="edge"> +<title>github.com/docker/docker/pkg/idtools->strings</title> +<path fill="none" stroke="#000000" d="M2196.4436,-15130.4512C2207.4822,-14917.561 2312.6571,-12873.5505 2368,-11219.5998 2370.3407,-11149.6482 2381.5889,-8754.6956 2426,-8700.5998 2512.9633,-8594.6725 2607.0756,-8699.9425 2727,-8633.5998 2838.2377,-8572.0627 3069.6522,-8339.0208 3129,-8226.5998 3191.9916,-8107.2764 3119.5993,-8045.4894 3187,-7928.5998 3285.661,-7757.497 3403.8677,-7794.8873 3522,-7636.5998 3559.1139,-7586.8703 3530.2368,-7546.6686 3580,-7509.5998 3725.0196,-7401.5741 3845.6828,-7571.2613 3983,-7453.5998 4038.3485,-7406.174 4080.0841,-7175.5694 4092.3227,-7100.9477"/> +<polygon fill="#000000" stroke="#000000" points="4094.0765,-7101.0654 4093.1527,-7095.8491 4090.622,-7100.5029 4094.0765,-7101.0654"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->sync --> +<g id="edge320" class="edge"> +<title>github.com/docker/docker/pkg/idtools->sync</title> +<path fill="none" stroke="#000000" d="M2196.5602,-15130.4547C2208.9424,-14917.6038 2326.375,-12873.9525 2368,-11219.5998 2369.5744,-11157.028 2385.2799,-2254.1348 2426,-2206.5998 2503.7675,-2115.8173 2837.4624,-2120.5998 2957,-2120.5998 2957,-2120.5998 2957,-2120.5998 3354.5,-2120.5998 3495.2936,-2120.5998 3884.7504,-2099.7542 3983,-2200.5998 4072.6193,-2292.5871 4026.0771,-2648.0436 4041,-2775.5998 4058.4134,-2924.444 4083.6636,-3102.8629 4092.6616,-3165.5048"/> +<polygon fill="#000000" stroke="#000000" points="4090.9374,-3165.8102 4093.3814,-3170.5101 4094.4018,-3165.3119 4090.9374,-3165.8102"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->os --> +<g id="edge313" class="edge"> +<title>github.com/docker/docker/pkg/idtools->os</title> +<path fill="none" stroke="#000000" d="M2196.5776,-15166.6539C2210.6891,-15402.4503 2359.7256,-17873.8367 2426,-18189.5998 2524.8049,-18660.3544 2542.595,-18795.1338 2785,-19210.5998 2921.8285,-19445.1148 2959.0697,-19523.0631 3187,-19670.5998 3337.173,-19767.8051 3850.9893,-19911.3225 3983,-19790.5998 4036.345,-19741.8162 4031.6848,-19214.285 4041,-19142.5998 4055.6255,-19030.0493 4080.9961,-18896.6878 4091.3739,-18843.8358"/> +<polygon fill="#000000" stroke="#000000" points="4093.1402,-18843.9233 4092.389,-18838.6794 4089.7061,-18843.2471 4093.1402,-18843.9233"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->path/filepath --> +<g id="edge315" class="edge"> +<title>github.com/docker/docker/pkg/idtools->path/filepath</title> +<path fill="none" stroke="#000000" d="M2196.1113,-15166.9648C2204.1929,-15407.1466 2291.8464,-17934.3682 2426,-18668.5998 2524.2989,-19206.5966 2436.4968,-19424.1159 2785,-19845.5998 2964.7214,-20062.957 3771.3047,-20450.9571 3983,-20264.5998 4049.7944,-20205.8001 4087.7966,-19538.7495 4094.812,-19404.1766"/> +<polygon fill="#000000" stroke="#000000" points="4096.5747,-19403.9743 4095.0858,-19398.8904 4093.0794,-19403.7932 4096.5747,-19403.9743"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->regexp --> +<g id="edge316" class="edge"> +<title>github.com/docker/docker/pkg/idtools->regexp</title> +<path fill="none" stroke="#000000" d="M2196.5606,-15130.4547C2208.9479,-14917.6039 2326.4265,-12873.9538 2368,-11219.5998 2371.1755,-11093.2344 2370.3848,-2232.1131 2426,-2118.5998 2473.3142,-2022.0293 3085.7326,-1556.7857 3187,-1520.5998 3520.1644,-1401.55 3715.5997,-1280.9353 3983,-1512.5998 4042.4743,-1564.1259 4085.8372,-2149.5912 4094.4377,-2274.32"/> +<polygon fill="#000000" stroke="#000000" points="4092.71,-2274.7056 4094.7982,-2279.574 4096.2018,-2274.4659 4092.71,-2274.7056"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->os/exec --> +<g id="edge314" class="edge"> +<title>github.com/docker/docker/pkg/idtools->os/exec</title> +<path fill="none" stroke="#000000" d="M2196.0533,-15166.6566C2200.8932,-15314.4629 2240.3867,-16340.138 2426,-17146.5998 2483.2617,-17395.3933 3000.5297,-19140.2269 3187,-19314.5998 3351.3756,-19468.3114 3647.9664,-19476.024 3747.7231,-19473.945"/> +<polygon fill="#000000" stroke="#000000" points="3747.929,-19475.6908 3752.8869,-19473.8248 3747.8475,-19472.1917 3747.929,-19475.6908"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->github.com/opencontainers/runc/libcontainer/user --> +<g id="edge311" class="edge"> +<title>github.com/docker/docker/pkg/idtools->github.com/opencontainers/runc/libcontainer/user</title> +<path fill="none" stroke="#000000" d="M2231.5653,-15130.4493C2276.0604,-15108.9488 2354.5967,-15073.8759 2426,-15056.5998 2731.0582,-14982.7908 2832.5665,-15092.728 3129,-14989.5998 3209.9632,-14961.433 3290.9106,-14898.4504 3330.1288,-14865.1659"/> +<polygon fill="#000000" stroke="#000000" points="3331.5815,-14866.2266 3334.2475,-14861.6489 3329.3087,-14863.5649 3331.5815,-14866.2266"/> +</g> +<!-- github.com/docker/docker/pkg/system --> +<g id="node102" class="node"> +<title>github.com/docker/docker/pkg/system</title> +<g id="a_node102"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/system" xlink:title="github.com/docker/docker/pkg/system" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2677.5,-15106.5998C2677.5,-15106.5998 2475.5,-15106.5998 2475.5,-15106.5998 2469.5,-15106.5998 2463.5,-15100.5998 2463.5,-15094.5998 2463.5,-15094.5998 2463.5,-15082.5998 2463.5,-15082.5998 2463.5,-15076.5998 2469.5,-15070.5998 2475.5,-15070.5998 2475.5,-15070.5998 2677.5,-15070.5998 2677.5,-15070.5998 2683.5,-15070.5998 2689.5,-15076.5998 2689.5,-15082.5998 2689.5,-15082.5998 2689.5,-15094.5998 2689.5,-15094.5998 2689.5,-15100.5998 2683.5,-15106.5998 2677.5,-15106.5998"/> +<text text-anchor="middle" x="2576.5" y="-15084.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/system</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/idtools->github.com/docker/docker/pkg/system --> +<g id="edge310" class="edge"> +<title>github.com/docker/docker/pkg/idtools->github.com/docker/docker/pkg/system</title> +<path fill="none" stroke="#000000" d="M2307.7519,-15130.9224C2355.0861,-15123.4682 2410.2214,-15114.7854 2458.1898,-15107.2313"/> +<polygon fill="#000000" stroke="#000000" points="2458.7372,-15108.9168 2463.4041,-15106.4102 2458.1927,-15105.4594 2458.7372,-15108.9168"/> +</g> +<!-- github.com/docker/docker/pkg/idtools->syscall --> +<g id="edge321" class="edge"> +<title>github.com/docker/docker/pkg/idtools->syscall</title> +<path fill="none" stroke="#000000" d="M2195.6592,-15166.6317C2197.4442,-15334.6269 2217.8441,-16642.937 2426,-17675.5998 2530.3769,-18193.4141 2586.1305,-18319.2359 2785,-18808.5998 2934.9603,-19177.6116 2849.6196,-19397.8636 3187,-19609.5998 3336.8311,-19703.6322 3853.6156,-19736.2279 3983,-19615.5998 4117.7496,-19489.9696 3944.6747,-18945.6407 4041,-18788.5998 4046.5039,-18779.6268 4055.4425,-18772.7809 4064.5288,-18767.7146"/> +<polygon fill="#000000" stroke="#000000" points="4065.3768,-18769.246 4068.9868,-18765.369 4063.747,-18766.1485 4065.3768,-18769.246"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->bufio --> +<g id="edge495" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->bufio</title> +<path fill="none" stroke="#000000" d="M3444.7196,-14861.6551C3599.7965,-14890.347 3907.5487,-14936.2734 3983,-14865.5998 4127.0043,-14730.7142 3938.2264,-14148.0307 4041,-13979.5998 4046.483,-13970.614 4055.4162,-13963.7647 4064.5039,-13958.6994"/> +<polygon fill="#000000" stroke="#000000" points="4065.3521,-13960.2306 4068.9631,-13956.3545 4063.7231,-13957.1328 4065.3521,-13960.2306"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->errors --> +<g id="edge496" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->errors</title> +<path fill="none" stroke="#000000" d="M3365.6584,-14825.3738C3397.7022,-14771.8567 3489.7636,-14609.5566 3522,-14460.5998 3546.3739,-14347.9736 3520.1248,-10411.0565 3580,-10312.5998 3685.5908,-10138.9698 3876.9732,-10269.9639 3983,-10096.5998 4071.4516,-9951.9729 4021.9543,-7214.0572 4041,-7045.5998 4051.1426,-6955.8894 4077.3853,-6851.5029 4089.587,-6805.9065"/> +<polygon fill="#000000" stroke="#000000" points="4091.3245,-6806.1838 4090.9334,-6800.9009 4087.9446,-6805.2747 4091.3245,-6806.1838"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->fmt --> +<g id="edge497" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->fmt</title> +<path fill="none" stroke="#000000" d="M3365.469,-14825.3315C3396.9992,-14771.6997 3487.8075,-14609.1198 3522,-14460.5998 3557.9006,-14304.6603 3504.9758,-13162.9411 3580,-13021.5998 3684.3168,-12825.073 3879.924,-12921.7802 3983,-12724.5998 4036.9401,-12621.4145 4037.6937,-10745.9863 4041,-10629.5998 4062.7412,-9864.2836 4090.5745,-8924.5009 4095.3028,-8765.0901"/> +<polygon fill="#000000" stroke="#000000" points="4097.0621,-8764.7999 4095.4613,-8759.7502 4093.5637,-8764.696 4097.0621,-8764.7999"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->io --> +<g id="edge499" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->io</title> +<path fill="none" stroke="#000000" d="M3498.1496,-14844.9619C3673.0401,-14845.1285 3949.3974,-14839.9971 3983,-14806.5998 4123.89,-14666.5706 3937.5706,-14084.1891 4041,-13914.5998 4046.481,-13905.6128 4055.4137,-13898.7632 4064.5015,-13893.6979"/> +<polygon fill="#000000" stroke="#000000" points="4065.3498,-13895.2291 4068.9608,-13891.3531 4063.7208,-13892.1313 4065.3498,-13895.2291"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->strconv --> +<g id="edge502" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->strconv</title> +<path fill="none" stroke="#000000" d="M3365.7063,-14825.3841C3397.8797,-14771.8948 3490.2576,-14609.6627 3522,-14460.5998 3544.8242,-14353.4167 3517.725,-6660.7717 3580,-6570.5998 3689.144,-6412.5635 3866.2041,-6567.0682 3983,-6414.5998 4048.5326,-6329.0519 4088.169,-5523.7217 4094.9559,-5375.1151"/> +<polygon fill="#000000" stroke="#000000" points="4096.7226,-5374.7855 4095.2014,-5369.7112 4093.2262,-5374.6266 4096.7226,-5374.7855"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->strings --> +<g id="edge503" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->strings</title> +<path fill="none" stroke="#000000" d="M3365.6565,-14825.3734C3397.6951,-14771.8552 3489.7439,-14609.5524 3522,-14460.5998 3545.9362,-14350.067 3506.9935,-10475.974 3580,-10389.5998 3697.5553,-10250.5199 3864.7376,-10454.079 3983,-10315.5998 4025.8201,-10265.4596 4038.1019,-8008.4724 4041,-7942.5998 4055.7173,-7608.0844 4086.4039,-7201.5647 4094.179,-7100.9517"/> +<polygon fill="#000000" stroke="#000000" points="4095.9329,-7100.9672 4094.5741,-7095.847 4092.4433,-7100.697 4095.9329,-7100.9672"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->os --> +<g id="edge500" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->os</title> +<path fill="none" stroke="#000000" d="M3362.0161,-14862.062C3389.9048,-14931.6357 3487.6165,-15185.1845 3522,-15404.5998 3546.0618,-15558.1482 3492.1908,-18073.3593 3580,-18201.5998 3689.4412,-18361.4327 3864.4645,-18214.3899 3983,-18367.5998 4098.5784,-18516.9876 3935.8559,-18631.6929 4041,-18788.5998 4046.6039,-18796.9626 4055.1871,-18803.4263 4063.9186,-18808.2834"/> +<polygon fill="#000000" stroke="#000000" points="4063.5249,-18810.0523 4068.7667,-18810.8183 4065.1467,-18806.9507 4063.5249,-18810.0523"/> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->golang.org/x/sys/unix --> +<g id="edge498" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M3364.5761,-14825.5419C3394.0583,-14771.8332 3480.6703,-14607.6709 3522,-14460.5998 3547.7878,-14368.8345 3513.5044,-14103.8948 3580,-14035.5998 3612.7932,-14001.9192 3665.6807,-13994.2772 3709.0452,-13994.735"/> +<polygon fill="#000000" stroke="#000000" points="3709.2535,-13996.489 3714.2844,-13994.8298 3709.3169,-13992.9896 3709.2535,-13996.489"/> +</g> +<!-- os/user --> +<g id="node122" class="node"> +<title>os/user</title> +<g id="a_node122"><a xlink:href="https://godoc.org/os/user" xlink:title="os/user" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3797,-14982.5998C3797,-14982.5998 3766,-14982.5998 3766,-14982.5998 3760,-14982.5998 3754,-14976.5998 3754,-14970.5998 3754,-14970.5998 3754,-14958.5998 3754,-14958.5998 3754,-14952.5998 3760,-14946.5998 3766,-14946.5998 3766,-14946.5998 3797,-14946.5998 3797,-14946.5998 3803,-14946.5998 3809,-14952.5998 3809,-14958.5998 3809,-14958.5998 3809,-14970.5998 3809,-14970.5998 3809,-14976.5998 3803,-14982.5998 3797,-14982.5998"/> +<text text-anchor="middle" x="3781.5" y="-14960.8998" font-family="Times,serif" font-size="14.00" fill="#000000">os/user</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/runc/libcontainer/user->os/user --> +<g id="edge501" class="edge"> +<title>github.com/opencontainers/runc/libcontainer/user->os/user</title> +<path fill="none" stroke="#000000" d="M3398.791,-14861.7282C3443.8099,-14879.625 3515.8612,-14906.8208 3580,-14924.5998 3638.5486,-14940.8292 3708.4017,-14953.1716 3748.5889,-14959.6136"/> +<polygon fill="#000000" stroke="#000000" points="3748.6059,-14961.3882 3753.8187,-14960.4454 3749.1557,-14957.9316 3748.6059,-14961.3882"/> +</g> +<!-- github.com/docker/docker/pkg/system->bufio --> +<g id="edge332" class="edge"> +<title>github.com/docker/docker/pkg/system->bufio</title> +<path fill="none" stroke="#000000" d="M2685.6298,-15106.7203C2700.4059,-15112.1972 2714.6934,-15119.3249 2727,-15128.5998 2767.1136,-15158.8316 2753.4864,-15186.4852 2785,-15225.5998 2938.6731,-15416.3385 2963.961,-15501.3564 3187,-15602.5998 3509.5289,-15749.0044 3728.2111,-15887.6522 3983,-15641.5998 4115.9175,-15513.24 3947.169,-14138.7823 4041,-13979.5998 4046.3454,-13970.5315 4055.2426,-13963.6606 4064.3396,-13958.6009"/> +<polygon fill="#000000" stroke="#000000" points="4065.1898,-13960.1312 4068.8068,-13956.2608 4063.5656,-13957.0308 4065.1898,-13960.1312"/> +</g> +<!-- github.com/docker/docker/pkg/system->errors --> +<g id="edge333" class="edge"> +<title>github.com/docker/docker/pkg/system->errors</title> +<path fill="none" stroke="#000000" d="M2577.803,-15070.3537C2590.186,-14896.1236 2687.5205,-13508.2134 2727,-12381.5998 2734.7736,-12159.7681 2723.5067,-8599.8797 2785,-8386.5998 2866.4469,-8104.1141 3039.2105,-8101.5456 3129,-7821.5998 3182.3553,-7655.2486 3100.4289,-7189.3396 3187,-7037.5998 3276.8725,-6880.0734 3419.1439,-6953.9728 3522,-6804.5998 3582.7669,-6716.3509 3495.6125,-6636.6244 3580,-6570.5998 3721.0654,-6460.2307 3829.4248,-6478.4291 3983,-6570.5998 4052.7292,-6612.449 4081.7928,-6713.5355 4091.6858,-6759.3439"/> +<polygon fill="#000000" stroke="#000000" points="4090.0047,-6759.854 4092.7454,-6764.3874 4093.4299,-6759.1343 4090.0047,-6759.854"/> +</g> +<!-- github.com/docker/docker/pkg/system->fmt --> +<g id="edge334" class="edge"> +<title>github.com/docker/docker/pkg/system->fmt</title> +<path fill="none" stroke="#000000" d="M2577.5072,-15070.5981C2592.7974,-14797.3719 2777.1622,-11505.3288 2785,-11482.5998 2872.4572,-11228.982 3042.9073,-11240.6841 3129,-10986.5998 3173.0138,-10856.7025 3104.1156,-9862.8734 3187,-9753.5998 3281.7883,-9628.6324 3421.1132,-9762.6979 3522,-9642.5998 3635.564,-9507.4105 3456.2968,-9375.5778 3580,-9249.5998 3707.2141,-9120.0464 3849.2049,-9305.345 3983,-9182.5998 4046.2576,-9124.5666 4083.5253,-8847.4811 4093.3624,-8764.8865"/> +<polygon fill="#000000" stroke="#000000" points="4095.1226,-8764.9024 4093.9707,-8759.7318 4091.6467,-8764.4922 4095.1226,-8764.9024"/> +</g> +<!-- github.com/docker/docker/pkg/system->io --> +<g id="edge340" class="edge"> +<title>github.com/docker/docker/pkg/system->io</title> +<path fill="none" stroke="#000000" d="M2661.3378,-15106.6446C2699.0388,-15113.994 2744.064,-15121.86 2785,-15126.5998 2917.3014,-15141.9186 3886.6694,-15258.5712 3983,-15166.5998 4083.7246,-15070.4334 3969.6003,-14034.1636 4041,-13914.5998 4046.397,-13905.5621 4055.3077,-13898.6993 4064.4013,-13893.6374"/> +<polygon fill="#000000" stroke="#000000" points="4065.2507,-13895.1681 4068.8655,-13891.2956 4063.6247,-13892.0687 4065.2507,-13895.1681"/> +</g> +<!-- github.com/docker/docker/pkg/system->io/ioutil --> +<g id="edge341" class="edge"> +<title>github.com/docker/docker/pkg/system->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2578.4901,-15106.6476C2588.4752,-15191.4983 2638.57,-15557.3068 2785,-15812.5998 2801.1101,-15840.6869 3158.2699,-16219.6667 3187,-16234.5998 3492.3398,-16393.3069 3934.9361,-16329.9889 4061.2001,-16307.3445"/> +<polygon fill="#000000" stroke="#000000" points="4061.659,-16309.0399 4066.2672,-16306.427 4061.0353,-16305.5959 4061.659,-16309.0399"/> +</g> +<!-- github.com/docker/docker/pkg/system->strconv --> +<g id="edge346" class="edge"> +<title>github.com/docker/docker/pkg/system->strconv</title> +<path fill="none" stroke="#000000" d="M2577.8169,-15070.3542C2590.3296,-14896.1285 2688.6343,-13508.2518 2727,-12381.5998 2735.7453,-12124.784 2725.8145,-8006.6556 2785,-7756.5998 2865.5778,-7416.1628 3011.8498,-7378.245 3129,-7048.5998 3166.7606,-6942.3464 3110.4552,-6884.4041 3187,-6801.5998 3293.097,-6686.8268 3425.7877,-6817.777 3522,-6694.5998 3644.9588,-6537.1799 3491.0885,-5976.4703 3580,-5797.5998 3690.9174,-5574.4583 3968.1769,-5416.7893 4063.3363,-5367.7432"/> +<polygon fill="#000000" stroke="#000000" points="4064.3478,-5369.1912 4067.9983,-5365.3523 4062.7506,-5366.0768 4064.3478,-5369.1912"/> +</g> +<!-- github.com/docker/docker/pkg/system->strings --> +<g id="edge347" class="edge"> +<title>github.com/docker/docker/pkg/system->strings</title> +<path fill="none" stroke="#000000" d="M2577.7827,-15070.353C2589.9763,-14896.1161 2685.8937,-13508.1552 2727,-12381.5998 2733.751,-12196.5838 2739.1623,-9228.9749 2785,-9049.5998 2866.5077,-8730.6388 3018.9488,-8703.8713 3129,-8393.5998 3172.648,-8270.5415 3108.4767,-8209.9194 3187,-8105.5998 3287.8979,-7971.5552 3423.4921,-8067.4105 3522,-7931.5998 3615.094,-7803.2532 3467.9662,-7691.7951 3580,-7579.5998 3708.2965,-7451.1183 3849.0017,-7635.1231 3983,-7512.5998 4045.4747,-7455.4751 4083.2245,-7182.6724 4093.2795,-7100.8494"/> +<polygon fill="#000000" stroke="#000000" points="4095.0343,-7100.9153 4093.9017,-7095.7404 4091.56,-7100.4921 4095.0343,-7100.9153"/> +</g> +<!-- github.com/docker/docker/pkg/system->time --> +<g id="edge349" class="edge"> +<title>github.com/docker/docker/pkg/system->time</title> +<path fill="none" stroke="#000000" d="M2578.8615,-15070.4768C2594.0728,-14953.2137 2679.1233,-14289.5816 2727,-13746.5998 2735.821,-13646.5588 2726.1378,-12925.971 2785,-12844.5998 2953.1901,-12612.0936 3810.4339,-12591.8769 3983,-12362.5998 4059.0333,-12261.5795 4026.1687,-11922.1634 4041,-11796.5998 4058.4953,-11648.4832 4083.6948,-11470.9241 4092.6705,-11408.5837"/> +<polygon fill="#000000" stroke="#000000" points="4094.4072,-11408.8011 4093.3885,-11403.6025 4090.943,-11408.3017 4094.4072,-11408.8011"/> +</g> +<!-- github.com/docker/docker/pkg/system->unsafe --> +<g id="edge350" class="edge"> +<title>github.com/docker/docker/pkg/system->unsafe</title> +<path fill="none" stroke="#000000" d="M2582.1279,-15106.8971C2603.0198,-15173.4116 2680.348,-15407.4924 2785,-15579.5998 2803.2049,-15609.5391 3155.2957,-16050.6796 3187,-16065.5998 3507.548,-16216.4512 3736.8837,-16278.4212 3983,-16023.5998 4055.724,-15948.3036 4091.2445,-14254.7098 4095.5521,-14035.0111"/> +<polygon fill="#000000" stroke="#000000" points="4097.3057,-14034.8391 4095.6538,-14029.8059 4093.8064,-14034.7708 4097.3057,-14034.8391"/> +</g> +<!-- github.com/docker/docker/pkg/system->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge337" class="edge"> +<title>github.com/docker/docker/pkg/system->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M2577.4901,-15070.5322C2592.3415,-14799.6831 2769.502,-11574.3165 2785,-11556.5998 2887.5546,-11439.3638 3015.7797,-11596.5713 3129,-11489.5998 3205.0032,-11417.7914 3122.9901,-11344.2779 3187,-11261.5998 3213.5327,-11227.3289 3256.6032,-11204.0778 3292.4243,-11189.5238"/> +<polygon fill="#000000" stroke="#000000" points="3293.2084,-11191.0951 3297.2031,-11187.6159 3291.9107,-11187.8446 3293.2084,-11191.0951"/> +</g> +<!-- github.com/docker/docker/pkg/system->github.com/pkg/errors --> +<g id="edge338" class="edge"> +<title>github.com/docker/docker/pkg/system->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M2577.7677,-15070.3524C2589.8209,-14896.1103 2684.6879,-13508.1105 2727,-12381.5998 2730.094,-12299.2255 2737.1294,-9481.708 2785,-9414.5998 2879.5549,-9282.0465 2986.1141,-9366.6679 3129,-9288.5998 3157.3943,-9273.0861 3158.8163,-9260.493 3187,-9244.5998 3326.5365,-9165.9132 3419.0634,-9234.3438 3522,-9111.5998 3604.222,-9013.5565 3486.2131,-8916.6456 3580,-8829.5998 3613.5387,-8798.4718 3664.8294,-8792.2152 3707.2683,-8793.6164"/> +<polygon fill="#000000" stroke="#000000" points="3707.3314,-8795.3703 3712.3982,-8793.8241 3707.4731,-8791.8731 3707.3314,-8795.3703"/> +</g> +<!-- github.com/docker/docker/pkg/system->os --> +<g id="edge342" class="edge"> +<title>github.com/docker/docker/pkg/system->os</title> +<path fill="none" stroke="#000000" d="M2578.5748,-15106.6789C2593.1428,-15234.2107 2680.8374,-16011.7128 2727,-16646.5998 2736.5677,-16778.1877 2726.7733,-17714.2082 2785,-17832.5998 2873.632,-18012.8142 3040.6814,-17945.2317 3129,-18125.5998 3191.4624,-18253.1633 3097.4193,-19292.376 3187,-19402.5998 3283.9482,-19521.8889 3393.5827,-19404.114 3522,-19488.5998 3557.2027,-19511.7597 3542.1726,-19545.034 3580,-19563.5998 3740.7889,-19642.5157 3847.9831,-19681.2911 3983,-19563.5998 4054.1898,-19501.5452 4026.6034,-19235.9352 4041,-19142.5998 4058.3018,-19030.4295 4082.1644,-18896.8538 4091.7564,-18843.8901"/> +<polygon fill="#000000" stroke="#000000" points="4093.5231,-18843.9546 4092.6933,-18838.7226 4090.0793,-18843.3302 4093.5231,-18843.9546"/> +</g> +<!-- github.com/docker/docker/pkg/system->path/filepath --> +<g id="edge344" class="edge"> +<title>github.com/docker/docker/pkg/system->path/filepath</title> +<path fill="none" stroke="#000000" d="M2576.7249,-15106.6099C2578.8574,-15246.7272 2599.8209,-16161.9516 2785,-16346.5998 2894.689,-16455.9744 3027.4335,-16285.6437 3129,-16402.5998 3230.7987,-16519.8233 3085.4511,-19079.1599 3187,-19196.5998 3285.7358,-19310.7865 3401.3469,-19161.8794 3522,-19252.5998 3569.6656,-19288.4402 3529.7496,-19341.4837 3580,-19373.5998 3727.7709,-19468.0433 3951.2501,-19422.1492 4048.261,-19395.3141"/> +<polygon fill="#000000" stroke="#000000" points="4049.0014,-19396.9243 4053.3457,-19393.8929 4048.0592,-19393.5535 4049.0014,-19396.9243"/> +</g> +<!-- github.com/docker/docker/pkg/system->golang.org/x/sys/unix --> +<g id="edge339" class="edge"> +<title>github.com/docker/docker/pkg/system->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M2689.832,-15104.5851C2896.8875,-15126.5103 3332.4224,-15137.3238 3522,-14876.5998 3632.1687,-14725.086 3453.8691,-14174.1082 3580,-14035.5998 3611.9447,-14000.5203 3665.5649,-13993.101 3709.464,-13994.0081"/> +<polygon fill="#000000" stroke="#000000" points="3709.4474,-13995.7582 3714.4944,-13994.1489 3709.5454,-13992.2596 3709.4474,-13995.7582"/> +</g> +<!-- github.com/docker/docker/pkg/system->runtime --> +<g id="edge345" class="edge"> +<title>github.com/docker/docker/pkg/system->runtime</title> +<path fill="none" stroke="#000000" d="M2576.9008,-15106.6531C2579.7976,-15205.1724 2603.0866,-15685.5609 2785,-16016.5998 2889.2813,-16206.3669 3042.6477,-16163.0314 3129,-16361.5998 3270.0019,-16685.8359 3038.6993,-17630.6365 3187,-17951.5998 3349.8587,-18304.0707 3681.0346,-18555.681 3983,-18311.5998 4023.4829,-18278.8772 4078.9643,-17904.8754 4092.7843,-17807.606"/> +<polygon fill="#000000" stroke="#000000" points="4094.5171,-17807.8502 4093.4858,-17802.6541 4091.0517,-17807.3592 4094.5171,-17807.8502"/> +</g> +<!-- github.com/docker/docker/pkg/system->os/exec --> +<g id="edge343" class="edge"> +<title>github.com/docker/docker/pkg/system->os/exec</title> +<path fill="none" stroke="#000000" d="M2578.2875,-15106.622C2590.0675,-15223.1838 2659.9695,-15882.1015 2785,-16405.5998 2900.7469,-16890.2279 3043.6243,-16981.7101 3129,-17472.5998 3145.982,-17570.242 3123.6885,-19179.35 3187,-19255.5998 3284.4051,-19372.9107 3399.4866,-19238.8251 3522,-19329.5998 3564.2122,-19360.8764 3537.4549,-19401.7775 3580,-19432.5998 3630.0962,-19468.8926 3704.808,-19474.3682 3747.7826,-19474.1258"/> +<polygon fill="#000000" stroke="#000000" points="3747.882,-19475.8749 3752.8623,-19474.0696 3747.8432,-19472.3751 3747.882,-19475.8749"/> +</g> +<!-- github.com/docker/docker/pkg/system->syscall --> +<g id="edge348" class="edge"> +<title>github.com/docker/docker/pkg/system->syscall</title> +<path fill="none" stroke="#000000" d="M2578.4639,-15106.6874C2592.2726,-15234.2775 2675.7158,-16012.106 2727,-16646.5998 2742.9855,-16844.3739 2723.867,-17348.8331 2785,-17537.5998 2873.4834,-17810.819 3042.9633,-17810.6004 3129,-18084.5998 3169.9883,-18215.1344 3107.5044,-19203.2456 3187,-19314.5998 3280.0772,-19444.9787 3388.1502,-19359.5869 3522,-19447.5998 3552.4602,-19467.6289 3546.3197,-19491.649 3580,-19505.5998 3745.4771,-19574.1427 3851.3424,-19627.0373 3983,-19505.5998 4100.5023,-19397.2188 3956.6918,-18924.4135 4041,-18788.5998 4046.4013,-18779.8988 4055.0202,-18773.1904 4063.8477,-18768.166"/> +<polygon fill="#000000" stroke="#000000" points="4065.168,-18769.4449 4068.7542,-18765.5459 4063.5193,-18766.3575 4065.168,-18769.4449"/> +</g> +<!-- github.com/docker/docker/pkg/mount --> +<g id="node104" class="node"> +<title>github.com/docker/docker/pkg/mount</title> +<g id="a_node104"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/mount" xlink:title="github.com/docker/docker/pkg/mount" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3056.5,-12307.5998C3056.5,-12307.5998 2857.5,-12307.5998 2857.5,-12307.5998 2851.5,-12307.5998 2845.5,-12301.5998 2845.5,-12295.5998 2845.5,-12295.5998 2845.5,-12283.5998 2845.5,-12283.5998 2845.5,-12277.5998 2851.5,-12271.5998 2857.5,-12271.5998 2857.5,-12271.5998 3056.5,-12271.5998 3056.5,-12271.5998 3062.5,-12271.5998 3068.5,-12277.5998 3068.5,-12283.5998 3068.5,-12283.5998 3068.5,-12295.5998 3068.5,-12295.5998 3068.5,-12301.5998 3062.5,-12307.5998 3056.5,-12307.5998"/> +<text text-anchor="middle" x="2957" y="-12285.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/mount</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/system->github.com/docker/docker/pkg/mount --> +<g id="edge335" class="edge"> +<title>github.com/docker/docker/pkg/system->github.com/docker/docker/pkg/mount</title> +<path fill="none" stroke="#000000" d="M2577.2112,-15070.317C2584.9592,-14874.3768 2655.9151,-13159.8341 2785,-12667.5998 2822.357,-12525.148 2909.5989,-12369.3749 2943.3392,-12312.2252"/> +<polygon fill="#000000" stroke="#000000" points="2945.0119,-12312.8353 2946.0556,-12307.6417 2942.0009,-12311.0508 2945.0119,-12312.8353"/> +</g> +<!-- github.com/docker/go-units --> +<g id="node105" class="node"> +<title>github.com/docker/go-units</title> +<g id="a_node105"><a xlink:href="https://godoc.org/github.com/docker/go-units" xlink:title="github.com/docker/go-units" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M3853,-7629.5998C3853,-7629.5998 3710,-7629.5998 3710,-7629.5998 3704,-7629.5998 3698,-7623.5998 3698,-7617.5998 3698,-7617.5998 3698,-7605.5998 3698,-7605.5998 3698,-7599.5998 3704,-7593.5998 3710,-7593.5998 3710,-7593.5998 3853,-7593.5998 3853,-7593.5998 3859,-7593.5998 3865,-7599.5998 3865,-7605.5998 3865,-7605.5998 3865,-7617.5998 3865,-7617.5998 3865,-7623.5998 3859,-7629.5998 3853,-7629.5998"/> +<text text-anchor="middle" x="3781.5" y="-7607.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go-units</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/system->github.com/docker/go-units --> +<g id="edge336" class="edge"> +<title>github.com/docker/docker/pkg/system->github.com/docker/go-units</title> +<path fill="none" stroke="#000000" d="M2577.7736,-15070.3526C2589.8818,-14896.1126 2685.1601,-13508.1282 2727,-12381.5998 2730.1966,-12295.533 2743.6268,-9357.1378 2785,-9281.5998 2873.2299,-9120.5122 3012.2337,-9194.3721 3129,-9052.5998 3417.9232,-8701.8026 3389.0233,-8543.1715 3522,-8108.5998 3560.0776,-7984.161 3519.6674,-7936.9033 3580,-7821.5998 3622.8903,-7739.6309 3707.5018,-7667.4916 3752.2162,-7633.0507"/> +<polygon fill="#000000" stroke="#000000" points="3753.5091,-7634.2649 3756.4161,-7629.8363 3751.3819,-7631.4855 3753.5091,-7634.2649"/> +</g> +<!-- github.com/docker/docker/pkg/mount->bufio --> +<g id="edge322" class="edge"> +<title>github.com/docker/docker/pkg/mount->bufio</title> +<path fill="none" stroke="#000000" d="M2961.6392,-12307.7881C2985.0374,-12400.4844 3090.4936,-12829.9414 3129,-13188.5998 3142.8342,-13317.4549 3101.9492,-15422.8175 3187,-15520.5998 3303.4748,-15654.51 3855.2598,-15708.81 3983,-15585.5998 4111.5198,-15461.6377 3950.2332,-14133.3702 4041,-13979.5998 4046.3509,-13970.5347 4055.2495,-13963.6647 4064.3462,-13958.6048"/> +<polygon fill="#000000" stroke="#000000" points="4065.1963,-13960.1351 4068.8131,-13956.2645 4063.5719,-13957.0348 4065.1963,-13960.1351"/> +</g> +<!-- github.com/docker/docker/pkg/mount->fmt --> +<g id="edge323" class="edge"> +<title>github.com/docker/docker/pkg/mount->fmt</title> +<path fill="none" stroke="#000000" d="M3068.7474,-12290.0554C3091.5757,-12284.8192 3113.3657,-12274.9938 3129,-12257.5998 3240.3026,-12133.7699 3096.7303,-11650.5052 3187,-11510.5998 3279.0965,-11367.8632 3432.3831,-11470.9061 3522,-11326.5998 3680.4154,-11071.5103 3421.7427,-10231.7875 3580,-9976.5998 3687.7737,-9802.8162 3869.368,-9924.6106 3983,-9754.5998 4039.9163,-9669.4444 4086.3083,-8909.7679 4094.6594,-8765.2758"/> +<polygon fill="#000000" stroke="#000000" points="4096.4222,-8765.1022 4094.9626,-8760.0098 4092.928,-8764.9009 4096.4222,-8765.1022"/> +</g> +<!-- github.com/docker/docker/pkg/mount->io --> +<g id="edge327" class="edge"> +<title>github.com/docker/docker/pkg/mount->io</title> +<path fill="none" stroke="#000000" d="M2961.5737,-12307.7953C2984.6523,-12400.5267 3088.7939,-12830.128 3129,-13188.5998 3139.4587,-13281.8482 3129.9723,-14802.0848 3187,-14876.5998 3295.7404,-15018.6851 3853.1449,-15120.6866 3983,-14997.5998 4070.4592,-14914.6991 3978.8432,-14017.8383 4041,-13914.5998 4046.4296,-13905.5816 4055.3488,-13898.7239 4064.4401,-13893.6607"/> +<polygon fill="#000000" stroke="#000000" points="4065.2891,-13895.1916 4068.9024,-13891.3177 4063.662,-13892.0928 4065.2891,-13895.1916"/> +</g> +<!-- github.com/docker/docker/pkg/mount->sort --> +<g id="edge329" class="edge"> +<title>github.com/docker/docker/pkg/mount->sort</title> +<path fill="none" stroke="#000000" d="M3068.7534,-12291.0348C3091.8677,-12285.8188 3113.7804,-12275.7559 3129,-12257.5998 3251.7764,-12111.1348 3083.5919,-5538.3257 3187,-5377.5998 3275.8656,-5239.477 3429.6465,-5357.4153 3522,-5221.5998 3583.0865,-5131.7657 3500.9373,-4320.1036 3580,-4245.5998 3710.3531,-4122.7631 3836.8502,-4142.0574 3983,-4245.5998 4046.4272,-4290.536 3991.4644,-4350.6957 4041,-4410.5998 4047.2369,-4418.1422 4055.7441,-4424.2967 4064.2031,-4429.1206"/> +<polygon fill="#000000" stroke="#000000" points="4063.6542,-4430.8139 4068.883,-4431.664 4065.3255,-4427.7387 4063.6542,-4430.8139"/> +</g> +<!-- github.com/docker/docker/pkg/mount->strconv --> +<g id="edge330" class="edge"> +<title>github.com/docker/docker/pkg/mount->strconv</title> +<path fill="none" stroke="#000000" d="M3068.7094,-12290.9979C3091.8255,-12285.7834 3113.75,-12275.7304 3129,-12257.5998 3217.0669,-12152.8975 3135.102,-7459.1895 3187,-7332.5998 3268.4364,-7133.9599 3433.7989,-7180.3299 3522,-6984.5998 3587.9472,-6838.254 3489.994,-6397.5097 3580,-6264.5998 3692.119,-6099.0361 3864.6079,-6225.7374 3983,-6064.5998 4066.5563,-5950.8755 4090.5506,-5485.4868 4095.1149,-5375.1427"/> +<polygon fill="#000000" stroke="#000000" points="4096.875,-5374.9282 4095.3297,-5369.8612 4093.3779,-5374.7859 4096.875,-5374.9282"/> +</g> +<!-- github.com/docker/docker/pkg/mount->strings --> +<g id="edge331" class="edge"> +<title>github.com/docker/docker/pkg/mount->strings</title> +<path fill="none" stroke="#000000" d="M3068.5264,-12290.8428C3091.65,-12285.6347 3113.6239,-12275.6235 3129,-12257.5998 3210.2626,-12162.345 3121.9842,-10111.6046 3187,-10004.5998 3275.3178,-9859.2439 3423.0886,-9957.965 3522,-9819.5998 3608.0917,-9699.1678 3488.536,-9608.0042 3580,-9491.5998 3700.8643,-9337.7783 3872.606,-9476.1003 3983,-9314.5998 4026.0517,-9251.6176 4036.9681,-8018.7835 4041,-7942.5998 4058.6959,-7608.2288 4087.183,-7201.6024 4094.3318,-7100.9591"/> +<polygon fill="#000000" stroke="#000000" points="4096.0858,-7100.9644 4094.6949,-7095.8529 4092.5946,-7100.7161 4096.0858,-7100.9644"/> +</g> +<!-- github.com/docker/docker/pkg/mount->github.com/pkg/errors --> +<g id="edge324" class="edge"> +<title>github.com/docker/docker/pkg/mount->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M3068.55,-12290.863C3091.6726,-12285.654 3113.6401,-12275.6374 3129,-12257.5998 3216.2978,-12155.0834 3148.8672,-9963.737 3187,-9834.5998 3267.6275,-9561.5536 3380.3374,-9535.5547 3522,-9288.5998 3622.464,-9113.4649 3733.9852,-8896.8022 3769.7751,-8826.6621"/> +<polygon fill="#000000" stroke="#000000" points="3771.5075,-8827.1171 3772.22,-8821.8678 3768.3895,-8825.527 3771.5075,-8827.1171"/> +</g> +<!-- github.com/docker/docker/pkg/mount->github.com/sirupsen/logrus --> +<g id="edge325" class="edge"> +<title>github.com/docker/docker/pkg/mount->github.com/sirupsen/logrus</title> +<path fill="none" stroke="#000000" d="M3068.519,-12294.0886C3131.1611,-12296.6101 3208.2258,-12299.7121 3266.5666,-12302.0604"/> +<polygon fill="#000000" stroke="#000000" points="3266.7305,-12303.8183 3271.7969,-12302.2709 3266.8713,-12300.3211 3266.7305,-12303.8183"/> +</g> +<!-- github.com/docker/docker/pkg/mount->os --> +<g id="edge328" class="edge"> +<title>github.com/docker/docker/pkg/mount->os</title> +<path fill="none" stroke="#000000" d="M2961.7393,-12307.7777C2985.6268,-12400.4233 3093.0949,-12829.6717 3129,-13188.5998 3136.7353,-13265.9264 3140.2958,-18721.4874 3187,-18783.5998 3294.4697,-18926.5248 3812.7544,-18956.3146 3983,-18901.5998 4019.1279,-18889.9888 4053.4993,-18862.0902 4074.6462,-18842.2882"/> +<polygon fill="#000000" stroke="#000000" points="4076.0153,-18843.4015 4078.4383,-18838.6907 4073.6064,-18840.8623 4076.0153,-18843.4015"/> +</g> +<!-- github.com/docker/docker/pkg/mount->golang.org/x/sys/unix --> +<g id="edge326" class="edge"> +<title>github.com/docker/docker/pkg/mount->golang.org/x/sys/unix</title> +<path fill="none" stroke="#000000" d="M2961.1868,-12307.8442C2982.3752,-12400.8145 3078.743,-12831.3984 3129,-13188.5998 3148.8654,-13329.7929 3096.5134,-13717.4077 3187,-13827.5998 3316.6406,-13985.4725 3581.0009,-14005.4292 3708.9326,-14005.2046"/> +<polygon fill="#000000" stroke="#000000" points="3709.1802,-14006.9537 3714.1732,-14005.1839 3709.1663,-14003.4537 3709.1802,-14006.9537"/> +</g> +<!-- github.com/docker/go-units->fmt --> +<g id="edge377" class="edge"> +<title>github.com/docker/go-units->fmt</title> +<path fill="none" stroke="#000000" d="M3865.2377,-7606.5713C3906.5537,-7608.8486 3953.6906,-7619.4405 3983,-7651.5998 4020.3619,-7692.5947 4083.4409,-8563.6475 4094.3644,-8718.2456"/> +<polygon fill="#000000" stroke="#000000" points="4092.6333,-8718.5759 4094.7309,-8723.4404 4096.1246,-8718.3296 4092.6333,-8718.5759"/> +</g> +<!-- github.com/docker/go-units->strconv --> +<g id="edge379" class="edge"> +<title>github.com/docker/go-units->strconv</title> +<path fill="none" stroke="#000000" d="M3786.0688,-7593.4549C3810.3878,-7496.1833 3925.6695,-7026.3297 3983,-6635.5998 4057.3381,-6128.9569 4089.0659,-5502.1522 4094.9636,-5374.8016"/> +<polygon fill="#000000" stroke="#000000" points="4096.7122,-5374.8706 4095.194,-5369.7954 4093.2159,-5374.7096 4096.7122,-5374.8706"/> +</g> +<!-- github.com/docker/go-units->strings --> +<g id="edge380" class="edge"> +<title>github.com/docker/go-units->strings</title> +<path fill="none" stroke="#000000" d="M3865.1873,-7615.0131C3905.724,-7612.1636 3952.1748,-7601.5464 3983,-7571.5998 4052.2954,-7504.2795 4085.8615,-7188.8799 4094.0012,-7100.6089"/> +<polygon fill="#000000" stroke="#000000" points="4095.7459,-7100.7462 4094.4575,-7095.6079 4092.2603,-7100.4282 4095.7459,-7100.7462"/> +</g> +<!-- github.com/docker/go-units->time --> +<g id="edge381" class="edge"> +<title>github.com/docker/go-units->time</title> +<path fill="none" stroke="#000000" d="M3809.3128,-7629.8435C3855.6042,-7662.0655 3946.9305,-7733.9964 3983,-7821.5998 4057.7172,-8003.0685 3941.7666,-11184.2886 4041,-11353.5998 4046.2805,-11362.6094 4055.1608,-11369.3286 4064.2622,-11374.2203"/> +<polygon fill="#000000" stroke="#000000" points="4063.481,-11375.7862 4068.7332,-11376.4769 4065.0581,-11372.6616 4063.481,-11375.7862"/> +</g> +<!-- github.com/docker/go-units->regexp --> +<g id="edge378" class="edge"> +<title>github.com/docker/go-units->regexp</title> +<path fill="none" stroke="#000000" d="M3786.5728,-7593.5195C3813.4875,-7496.5805 3939.9669,-7028.1616 3983,-6635.5998 4078.055,-5768.4768 3976.8689,-3580.5567 4041,-2710.5998 4052.0174,-2561.146 4081.2239,-2383.1042 4091.9636,-2320.6283"/> +<polygon fill="#000000" stroke="#000000" points="4093.6992,-2320.8612 4092.8248,-2315.6365 4090.2502,-2320.2662 4093.6992,-2320.8612"/> +</g> +<!-- golang.org/x/net/proxy->context --> +<g id="edge655" class="edge"> +<title>golang.org/x/net/proxy->context</title> +<path fill="none" stroke="#000000" d="M3359.345,-7534.371C3383.0484,-7444.1641 3487.063,-7036.1982 3522,-6694.5998 3536.8415,-6549.4868 3514.8399,-1574.1074 3580,-1443.5998 3680.9417,-1241.426 3834.7175,-1304.1161 3983,-1133.5998 4031.7782,-1077.5076 4069.6159,-997.1098 4086.4499,-957.8961"/> +<polygon fill="#000000" stroke="#000000" points="4088.2415,-958.1552 4088.5894,-952.8692 4085.021,-956.7845 4088.2415,-958.1552"/> +</g> +<!-- golang.org/x/net/proxy->errors --> +<g id="edge656" class="edge"> +<title>golang.org/x/net/proxy->errors</title> +<path fill="none" stroke="#000000" d="M3425.0537,-7551.2047C3458.1793,-7547.0217 3495.9989,-7536.6649 3522,-7512.5998 3583.6849,-7455.5081 3536.4804,-7404.5063 3580,-7332.5998 3712.2981,-7114.0067 3813.0012,-7113.3514 3983,-6922.5998 4018.8515,-6882.3717 4057.6994,-6832.7845 4079.2241,-6804.7208"/> +<polygon fill="#000000" stroke="#000000" points="4080.6218,-6805.774 4082.2718,-6800.7401 4077.8427,-6803.6463 4080.6218,-6805.774"/> +</g> +<!-- golang.org/x/net/proxy->strings --> +<g id="edge661" class="edge"> +<title>golang.org/x/net/proxy->strings</title> +<path fill="none" stroke="#000000" d="M3425.1497,-7548.5321C3457.3236,-7543.6535 3494.3109,-7533.4828 3522,-7512.5998 3563.9447,-7480.9653 3537.2346,-7440.1157 3580,-7409.5998 3728.2359,-7303.8237 3840.2378,-7448.6548 3983,-7335.5998 4059.5331,-7274.9923 4085.4205,-7152.0054 4093.1093,-7100.7933"/> +<polygon fill="#000000" stroke="#000000" points="4094.851,-7100.976 4093.8384,-7095.7763 4091.3874,-7100.4726 4094.851,-7100.976"/> +</g> +<!-- golang.org/x/net/proxy->sync --> +<g id="edge662" class="edge"> +<title>golang.org/x/net/proxy->sync</title> +<path fill="none" stroke="#000000" d="M3359.3017,-7534.3665C3382.7999,-7444.1382 3485.9937,-7036.0871 3522,-6694.5998 3531.762,-6602.0158 3523.7964,-3417.8174 3580,-3343.5998 3698.7412,-3186.8007 3970.5496,-3183.0121 4063.8593,-3186.6611"/> +<polygon fill="#000000" stroke="#000000" points="4063.8888,-3188.4138 4068.9578,-3186.8751 4064.0356,-3184.9169 4063.8888,-3188.4138"/> +</g> +<!-- golang.org/x/net/proxy->net/url --> +<g id="edge659" class="edge"> +<title>golang.org/x/net/proxy->net/url</title> +<path fill="none" stroke="#000000" d="M3359.3386,-7534.3703C3383.0117,-7444.1603 3486.9049,-7036.182 3522,-6694.5998 3535.756,-6560.7117 3514.2542,-1967.0424 3580,-1849.5998 3689.108,-1654.6989 3968.8344,-1560.3098 4063.8551,-1533.1367"/> +<polygon fill="#000000" stroke="#000000" points="4064.4426,-1534.7892 4068.7762,-1531.7424 4063.4885,-1531.4217 4064.4426,-1534.7892"/> +</g> +<!-- golang.org/x/net/proxy->os --> +<g id="edge660" class="edge"> +<title>golang.org/x/net/proxy->os</title> +<path fill="none" stroke="#000000" d="M3356.1482,-7570.6329C3371.0607,-7734.7248 3483.0288,-8986.6314 3522,-10004.5998 3529.4048,-10198.0204 3515.192,-16790.2094 3580,-16972.5998 3676.0271,-17242.8508 3882.1437,-17208.1137 3983,-17476.5998 4034.3136,-17613.2 3965.4752,-18663.7451 4041,-18788.5998 4046.4051,-18797.5353 4055.3179,-18804.235 4064.4109,-18809.1318"/> +<polygon fill="#000000" stroke="#000000" points="4063.6234,-18810.6946 4068.8746,-18811.3927 4065.2049,-18807.5722 4063.6234,-18810.6946"/> +</g> +<!-- golang.org/x/net/proxy->net --> +<g id="edge658" class="edge"> +<title>golang.org/x/net/proxy->net</title> +<path fill="none" stroke="#000000" d="M3356.1476,-7570.6329C3371.0545,-7734.7251 3482.9827,-8986.6332 3522,-10004.5998 3525.6669,-10100.2691 3521.3122,-16821.9572 3580,-16897.5998 3693.5981,-17044.0163 3842.8987,-16883.2988 3983,-17004.5998 4032.6787,-17047.6121 3997.4989,-17091.3487 4041,-17140.5998 4047.4174,-17147.8654 4055.8975,-17153.9099 4064.2722,-17158.7168"/> +<polygon fill="#000000" stroke="#000000" points="4063.6754,-17160.3856 4068.9002,-17161.2598 4065.361,-17157.3182 4063.6754,-17160.3856"/> +</g> +<!-- golang.org/x/net/internal/socks --> +<g id="node136" class="node"> +<title>golang.org/x/net/internal/socks</title> +<g id="a_node136"><a xlink:href="https://godoc.org/golang.org/x/net/internal/socks" xlink:title="golang.org/x/net/internal/socks" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3862,-7871.5998C3862,-7871.5998 3701,-7871.5998 3701,-7871.5998 3695,-7871.5998 3689,-7865.5998 3689,-7859.5998 3689,-7859.5998 3689,-7847.5998 3689,-7847.5998 3689,-7841.5998 3695,-7835.5998 3701,-7835.5998 3701,-7835.5998 3862,-7835.5998 3862,-7835.5998 3868,-7835.5998 3874,-7841.5998 3874,-7847.5998 3874,-7847.5998 3874,-7859.5998 3874,-7859.5998 3874,-7865.5998 3868,-7871.5998 3862,-7871.5998"/> +<text text-anchor="middle" x="3781.5" y="-7849.8998" font-family="Times,serif" font-size="14.00" fill="#000000">golang.org/x/net/internal/socks</text> +</a> +</g> +</g> +<!-- golang.org/x/net/proxy->golang.org/x/net/internal/socks --> +<g id="edge657" class="edge"> +<title>golang.org/x/net/proxy->golang.org/x/net/internal/socks</title> +<path fill="none" stroke="#000000" d="M3403.3038,-7570.6661C3440.5285,-7586.7227 3490.5922,-7613.576 3522,-7651.5998 3570.7029,-7710.5619 3522.0776,-7763.6649 3580,-7813.5998 3608.3856,-7838.0711 3647.4189,-7849.1922 3683.5759,-7853.7998"/> +<polygon fill="#000000" stroke="#000000" points="3683.716,-7855.5789 3688.8881,-7854.4332 3684.1305,-7852.1035 3683.716,-7855.5789"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->bytes --> +<g id="edge509" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->bytes</title> +<path fill="none" stroke="#000000" d="M2578.537,-6956.7954C2593.7805,-7093.8477 2690.4236,-7978.5014 2727,-8700.5998 2732.0904,-8801.095 2739.9799,-12232.6088 2785,-12322.5998 2935.666,-12623.767 3799.0632,-12968.5198 3983,-13250.5998 4031.1333,-13324.4156 4078.8532,-13599.2647 4092.2803,-13681.3238"/> +<polygon fill="#000000" stroke="#000000" points="4090.5834,-13681.7924 4093.1147,-13686.446 4094.0379,-13681.2296 4090.5834,-13681.7924"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->encoding/json --> +<g id="edge510" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->encoding/json</title> +<path fill="none" stroke="#000000" d="M2578.5727,-6956.7936C2594.0775,-7093.8331 2692.2783,-7978.4099 2727,-8700.5998 2741.229,-8996.5533 2723.276,-13743.805 2785,-14033.5998 2865.1164,-14409.7473 3044.1922,-14452.4822 3129,-14827.5998 3208.2904,-15178.3128 3091.4674,-16097.9587 3187,-16444.5998 3283.4471,-16794.5595 3282.1448,-16953.1001 3580,-17160.5998 3723.835,-17260.8019 3945.1062,-17253.5063 4044.5215,-17243.9808"/> +<polygon fill="#000000" stroke="#000000" points="4044.935,-17245.6986 4049.7398,-17243.4679 4044.5926,-17242.2154 4044.935,-17245.6986"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->errors --> +<g id="edge511" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->errors</title> +<path fill="none" stroke="#000000" d="M2587.3794,-6920.3836C2615.5489,-6872.3019 2691.1624,-6737.2366 2727,-6614.5998 2754.1073,-6521.8383 2712.2501,-6247.2155 2785,-6183.5998 2824.4357,-6149.1155 3883.5331,-6107.6343 3983,-6190.5998 4072.5955,-6265.3316 4091.8444,-6659.1124 4095.3176,-6759.3629"/> +<polygon fill="#000000" stroke="#000000" points="4093.5718,-6759.5205 4095.4892,-6764.4587 4097.0699,-6759.4026 4093.5718,-6759.5205"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->expvar --> +<g id="edge512" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->expvar</title> +<path fill="none" stroke="#000000" d="M2617.8159,-6920.5389C2659.4454,-6902.8214 2725.8182,-6875.8758 2785,-6857.5998 2833.0222,-6842.77 2890.0745,-6830.5302 2924.8982,-6823.6447"/> +<polygon fill="#000000" stroke="#000000" points="2925.3021,-6825.3488 2929.8711,-6822.6679 2924.6275,-6821.9145 2925.3021,-6825.3488"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->fmt --> +<g id="edge513" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->fmt</title> +<path fill="none" stroke="#000000" d="M2578.1411,-6956.7212C2588.891,-7071.3525 2652.9528,-7695.0262 2785,-7821.5998 2897.3852,-7929.3265 3010.7652,-7786.328 3129,-7887.5998 3187.082,-7937.3489 3133.8542,-7994.6088 3187,-8049.5998 3298.3249,-8164.79 3384.9886,-8099.5943 3522,-8182.5998 3551.1089,-8200.2348 3549.3029,-8217.9026 3580,-8232.5998 3744.2504,-8311.2399 3849.6464,-8182.5866 3983,-8306.5998 4044.9911,-8364.2489 4083.0771,-8636.6871 4093.2458,-8718.3868"/> +<polygon fill="#000000" stroke="#000000" points="4091.526,-8718.74 4093.8752,-8723.488 4094.9997,-8718.3113 4091.526,-8718.74"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->io/ioutil --> +<g id="edge522" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2578.5685,-6956.7938C2594.0424,-7093.8348 2692.0591,-7978.4204 2727,-8700.5998 2740.5617,-8980.9015 2702.7331,-13483.2994 2785,-13751.5998 2866.3425,-14016.8853 3043.8605,-14008.5084 3129,-14272.5998 3178.2357,-14425.3223 3108.7041,-15575.5353 3187,-15715.5998 3289.6707,-15899.2688 3391.5218,-15886.0502 3580,-15979.5998 3749.5815,-16063.7703 3835.0498,-15999.4757 3983,-16117.5998 4037.5959,-16161.1894 4072.7474,-16238.8135 4087.7141,-16277.4397"/> +<polygon fill="#000000" stroke="#000000" points="4086.1896,-16278.3531 4089.6079,-16282.4 4089.4594,-16277.1047 4086.1896,-16278.3531"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->math --> +<g id="edge523" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->math</title> +<path fill="none" stroke="#000000" d="M2579.0511,-6920.3105C2593.9157,-6813.5668 2670.6406,-6260.1949 2727,-5807.5998 2740.8221,-5696.6009 2707.7413,-5388.488 2785,-5307.5998 2891.99,-5195.5837 3026.6884,-5367.9047 3129,-5251.5998 3255.9597,-5107.2757 3117.7293,-3701.9034 3187,-3522.5998 3334.4907,-3140.828 3673.6467,-2791.6376 3983,-3059.5998 4042.4047,-3111.0562 4085.8223,-3695.7789 4094.4354,-3820.3495"/> +<polygon fill="#000000" stroke="#000000" points="4092.7072,-3820.7288 4094.7963,-3825.5969 4096.199,-3820.4885 4092.7072,-3820.7288"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sort --> +<g id="edge528" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sort</title> +<path fill="none" stroke="#000000" d="M2578.8416,-6920.4194C2594.2618,-6802.4933 2683.7534,-6145.4614 2785,-5973.5998 2888.1177,-5798.562 3035.3165,-5852.8632 3129,-5672.5998 3184.8595,-5565.1163 3102.8896,-5217.7687 3187,-5130.5998 3291.4398,-5022.3624 3420.2313,-5193.3524 3522,-5082.5998 3672.9109,-4918.3669 3417.7944,-4233.6878 3580,-4080.5998 3710.2587,-3957.663 3845.8247,-3965.4315 3983,-4080.5998 4097.0489,-4176.3519 3955.8238,-4288.4498 4041,-4410.5998 4046.758,-4418.8572 4055.3856,-4425.2907 4064.1103,-4430.1524"/> +<polygon fill="#000000" stroke="#000000" points="4063.7093,-4431.9183 4068.9497,-4432.6932 4065.3363,-4428.8195 4063.7093,-4431.9183"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->strings --> +<g id="edge529" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->strings</title> +<path fill="none" stroke="#000000" d="M2722.0092,-6945.6844C2743.1367,-6946.4794 2764.6245,-6947.1603 2785,-6947.5998 2937.8533,-6950.8973 3000.5037,-7030.4474 3129,-6947.5998 3181.4921,-6913.7557 3134.5079,-6853.4439 3187,-6819.5998 3312.1345,-6738.9198 3374.2782,-6800.9939 3522,-6819.5998 3731.8365,-6846.0292 3805.074,-6823.266 3983,-6937.5998 4029.8354,-6967.6959 4066.2294,-7023.7472 4084.0186,-7055.0236"/> +<polygon fill="#000000" stroke="#000000" points="4082.5321,-7055.9507 4086.5072,-7059.4524 4085.5834,-7054.2361 4082.5321,-7055.9507"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sync --> +<g id="edge530" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sync</title> +<path fill="none" stroke="#000000" d="M2579.1342,-6920.3206C2594.4667,-6813.6337 2673.3791,-6260.5275 2727,-5807.5998 2741.6827,-5683.5768 2705.8593,-5345.2126 2785,-5248.5998 2886.4624,-5124.7373 3032.5395,-5269.3962 3129,-5141.5998 3291.5065,-4926.3017 3028.4096,-2932.7986 3187,-2714.5998 3292.5496,-2569.3779 3845.0105,-2461.7563 3983,-2576.5998 4029.653,-2615.4273 4081.468,-3057.6154 4093.4386,-3165.1414"/> +<polygon fill="#000000" stroke="#000000" points="4091.7199,-3165.5209 4094.0106,-3170.2975 4095.1985,-3165.135 4091.7199,-3165.5209"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->time --> +<g id="edge532" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->time</title> +<path fill="none" stroke="#000000" d="M2578.347,-6956.8063C2592.1982,-7093.9387 2680.5439,-7979.0697 2727,-8700.5998 2736.5469,-8848.8771 2714.8213,-9905.6333 2785,-10036.5998 2874.3908,-10203.4199 3010.8101,-10139.7796 3129,-10287.5998 3169.5257,-10338.2854 3136.9441,-10380.299 3187,-10421.5998 3304.6121,-10518.6409 3415.9449,-10386.0466 3522,-10495.5998 3619.4932,-10596.3087 3480.7196,-10706.6523 3580,-10805.5998 3708.9842,-10934.1515 3858.1418,-10747.037 3983,-10879.5998 4128.518,-11034.0973 3924.191,-11176.3977 4041,-11353.5998 4046.5917,-11362.0826 4055.2642,-11368.6004 4064.0822,-11373.4704"/> +<polygon fill="#000000" stroke="#000000" points="4063.7331,-11375.2605 4068.9774,-11376.0084 4065.3441,-11372.1533 4063.7331,-11375.2605"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->unicode/utf8 --> +<g id="edge533" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M2578.9462,-6920.297C2593.2202,-6813.4774 2667.1833,-6259.7509 2727,-5807.5998 2739.9635,-5709.6097 2716.1845,-5437.5544 2785,-5366.5998 2892.8429,-5255.4046 3026.7052,-5426.9194 3129,-5310.5998 3257.0389,-5165.0066 3047.6428,-3701.3994 3187,-3566.5998 3237.9887,-3517.2787 3919.9973,-3514.6599 3983,-3572.5998 4081.2735,-3662.9762 4017.3346,-4039.2015 4041,-4170.5998 4053.2372,-4238.5447 4076.6536,-4316.815 4088.5972,-4354.6955"/> +<polygon fill="#000000" stroke="#000000" points="4086.9564,-4355.3108 4090.1345,-4359.5489 4090.293,-4354.2539 4086.9564,-4355.3108"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/beorn7/perks/quantile --> +<g id="edge514" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/beorn7/perks/quantile</title> +<path fill="none" stroke="#000000" d="M2577.9303,-6920.3991C2586.3844,-6818.8166 2634.6907,-6312.8718 2785,-5932.5998 2892.791,-5659.8962 3044.7179,-5650.4605 3129,-5369.5998 3155.7783,-5280.3643 3124.9815,-3763.1251 3187,-3693.5998 3310.8029,-3554.8118 3550.3311,-3566.1232 3684.4716,-3585.8132"/> +<polygon fill="#000000" stroke="#000000" points="3684.39,-3587.5703 3689.5934,-3586.5766 3684.9061,-3584.1085 3684.39,-3587.5703"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/cespare/xxhash/v2 --> +<g id="edge515" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/cespare/xxhash/v2</title> +<path fill="none" stroke="#000000" d="M2578.1324,-6956.8218C2590.412,-7094.0676 2669.391,-7979.8744 2727,-8700.5998 2740.8313,-8873.6382 2732.395,-9314.1722 2785,-9479.5998 2874.8986,-9762.305 3009.6941,-9779.9934 3129,-10051.5998 3165.7067,-10135.1647 3130.1172,-10177.2219 3187,-10248.5998 3293.4049,-10382.1192 3419.104,-10300.3579 3522,-10436.5998 3588.9173,-10525.2033 3500.3647,-10602.226 3580,-10679.5998 3607.7781,-10706.5891 3648.1531,-10718.2535 3685.4796,-10722.7318"/> +<polygon fill="#000000" stroke="#000000" points="3685.3237,-10724.4752 3690.4874,-10723.2922 3685.7131,-10720.9969 3685.3237,-10724.4752"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->os --> +<g id="edge524" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->os</title> +<path fill="none" stroke="#000000" d="M2578.5842,-6956.7931C2594.1729,-7093.8285 2692.8736,-7978.3815 2727,-8700.5998 2743.4838,-9049.4477 2716.2587,-14644.1948 2785,-14986.5998 2864.7314,-15383.7475 3047.3111,-15433.8502 3129,-15830.5998 3146.0214,-15913.27 3131.9496,-18804.6191 3187,-18868.5998 3302.7789,-19003.1606 3819.2976,-19004.2503 3983,-18935.5998 4027.5937,-18916.899 4063.4611,-18871.0668 4082.0717,-18843.2381"/> +<polygon fill="#000000" stroke="#000000" points="4083.6186,-18844.0711 4084.9091,-18838.9332 4080.6963,-18842.1449 4083.6186,-18844.0711"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->path/filepath --> +<g id="edge525" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->path/filepath</title> +<path fill="none" stroke="#000000" d="M2578.5884,-6956.7929C2594.2081,-7093.8269 2693.0934,-7978.3711 2727,-8700.5998 2735.7672,-8887.3455 2728.4141,-15252.4177 2785,-15430.5998 2866.2003,-15686.2896 3046.4443,-15671.3444 3129,-15926.5998 3180.5713,-16086.054 3094.0039,-18803.1833 3187,-18942.5998 3277.2249,-19077.8619 3385.3071,-19001.5578 3522,-19089.5998 3551.1062,-19108.3467 3550.7603,-19123.0618 3580,-19141.5998 3744.1789,-19245.6898 3810.0096,-19222.9221 3983,-19311.5998 4011.3817,-19326.1487 4042.1925,-19345.196 4064.4619,-19359.5916"/> +<polygon fill="#000000" stroke="#000000" points="4063.6518,-19361.1521 4068.7987,-19362.406 4065.5571,-19358.2161 4063.6518,-19361.1521"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->runtime --> +<g id="edge526" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->runtime</title> +<path fill="none" stroke="#000000" d="M2578.5782,-6956.7933C2594.1233,-7093.8309 2692.5643,-7978.3962 2727,-8700.5998 2757.4407,-9339.0181 2678.7031,-13822.3574 2785,-14452.5998 2864.847,-14926.0189 3044.2515,-15004.0336 3129,-15476.5998 3202.8599,-15888.4501 3073.9405,-16955.7431 3187,-17358.5998 3268.4786,-17648.9266 3417.1104,-17665.8869 3522,-17948.5998 3568.4791,-18073.8767 3476.8034,-18158.7171 3580,-18243.5998 3649.1644,-18300.4899 3912.5109,-18298.8401 3983,-18243.5998 4053.5461,-18188.3149 4085.9722,-17893.2191 4093.971,-17807.7874"/> +<polygon fill="#000000" stroke="#000000" points="4095.7234,-17807.8411 4094.4414,-17802.7011 4092.2383,-17807.5187 4095.7234,-17807.8411"/> +</g> +<!-- github.com/golang/protobuf/proto --> +<g id="node110" class="node"> +<title>github.com/golang/protobuf/proto</title> +<g id="a_node110"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3870.5,-6620.5998C3870.5,-6620.5998 3692.5,-6620.5998 3692.5,-6620.5998 3686.5,-6620.5998 3680.5,-6614.5998 3680.5,-6608.5998 3680.5,-6608.5998 3680.5,-6596.5998 3680.5,-6596.5998 3680.5,-6590.5998 3686.5,-6584.5998 3692.5,-6584.5998 3692.5,-6584.5998 3870.5,-6584.5998 3870.5,-6584.5998 3876.5,-6584.5998 3882.5,-6590.5998 3882.5,-6596.5998 3882.5,-6596.5998 3882.5,-6608.5998 3882.5,-6608.5998 3882.5,-6614.5998 3876.5,-6620.5998 3870.5,-6620.5998"/> +<text text-anchor="middle" x="3781.5" y="-6598.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/golang/protobuf/proto --> +<g id="edge516" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M2722.1252,-6947.9459C2865.1591,-6953.8518 3070.2304,-6952.5023 3129,-6901.5998 3222.4327,-6820.6743 3092.7034,-6709.5171 3187,-6629.5998 3258.0795,-6569.3593 3524.0957,-6580.6 3675.0868,-6592.5014"/> +<polygon fill="#000000" stroke="#000000" points="3675.2859,-6594.2727 3680.4089,-6592.9249 3675.5636,-6590.7837 3675.2859,-6594.2727"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sync/atomic --> +<g id="edge531" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sync/atomic</title> +<path fill="none" stroke="#000000" d="M2588.3402,-6956.8919C2662.0426,-7068.9162 3071.4355,-7664.7998 3580,-7886.5998 3742.0767,-7957.2862 3955.8591,-7971.3578 4049.1233,-7974.0408"/> +<polygon fill="#000000" stroke="#000000" points="4049.2368,-7975.7944 4054.2826,-7974.1811 4049.332,-7972.2957 4049.2368,-7975.7944"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->runtime/debug --> +<g id="edge527" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->runtime/debug</title> +<path fill="none" stroke="#000000" d="M2578.4392,-6956.8007C2592.9657,-7093.8919 2685.3362,-7978.7771 2727,-8700.5998 2733.1686,-8807.4704 2734.4333,-10532.2473 2785,-10626.5998 2873.5432,-10791.8127 3028.8924,-10712.1269 3129,-10870.5998 3208.9981,-10997.2388 3099.5965,-11080.9538 3187,-11202.5998 3214.5366,-11240.9247 3263.3671,-11264.7888 3301.2252,-11278.411"/> +<polygon fill="#000000" stroke="#000000" points="3300.9578,-11280.1722 3306.2552,-11280.1842 3302.1215,-11276.8713 3300.9578,-11280.1722"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal --> +<g id="node123" class="node"> +<title>github.com/prometheus/client_golang/prometheus/internal</title> +<g id="a_node123"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" xlink:title="github.com/prometheus/client_golang/prometheus/internal" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3113,-6233.5998C3113,-6233.5998 2801,-6233.5998 2801,-6233.5998 2795,-6233.5998 2789,-6227.5998 2789,-6221.5998 2789,-6221.5998 2789,-6209.5998 2789,-6209.5998 2789,-6203.5998 2795,-6197.5998 2801,-6197.5998 2801,-6197.5998 3113,-6197.5998 3113,-6197.5998 3119,-6197.5998 3125,-6203.5998 3125,-6209.5998 3125,-6209.5998 3125,-6221.5998 3125,-6221.5998 3125,-6227.5998 3119,-6233.5998 3113,-6233.5998"/> +<text text-anchor="middle" x="2957" y="-6211.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/internal</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_golang/prometheus/internal --> +<g id="edge517" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_golang/prometheus/internal</title> +<path fill="none" stroke="#000000" d="M2587.1945,-6920.3282C2614.9176,-6872.1129 2689.5462,-6736.7527 2727,-6614.5998 2774.3793,-6460.0758 2674.6842,-6373.7222 2785,-6255.5998 2792.283,-6247.8014 2800.765,-6241.3841 2809.9852,-6236.1165"/> +<polygon fill="#000000" stroke="#000000" points="2811.0136,-6237.5493 2814.5688,-6233.622 2809.3405,-6234.4751 2811.0136,-6237.5493"/> +</g> +<!-- github.com/prometheus/client_model/go --> +<g id="node124" class="node"> +<title>github.com/prometheus/client_model/go</title> +<g id="a_node124"><a xlink:href="https://godoc.org/github.com/prometheus/client_model/go" xlink:title="github.com/prometheus/client_model/go" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3462,-6555.5998C3462,-6555.5998 3247,-6555.5998 3247,-6555.5998 3241,-6555.5998 3235,-6549.5998 3235,-6543.5998 3235,-6543.5998 3235,-6531.5998 3235,-6531.5998 3235,-6525.5998 3241,-6519.5998 3247,-6519.5998 3247,-6519.5998 3462,-6519.5998 3462,-6519.5998 3468,-6519.5998 3474,-6525.5998 3474,-6531.5998 3474,-6531.5998 3474,-6543.5998 3474,-6543.5998 3474,-6549.5998 3468,-6555.5998 3462,-6555.5998"/> +<text text-anchor="middle" x="3354.5" y="-6533.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_model/go</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_model/go --> +<g id="edge518" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M2714.5005,-6920.5731C2873.1938,-6899.2582 3113.9692,-6864.8774 3129,-6850.5998 3218.9351,-6765.1718 3100.396,-6666.4031 3187,-6577.5998 3198.9178,-6565.3794 3213.851,-6556.6203 3229.8618,-6550.3864"/> +<polygon fill="#000000" stroke="#000000" points="3230.7836,-6551.9111 3234.8632,-6548.5318 3229.5666,-6548.6294 3230.7836,-6551.9111"/> +</g> +<!-- github.com/prometheus/common/expfmt --> +<g id="node125" class="node"> +<title>github.com/prometheus/common/expfmt</title> +<g id="a_node125"><a xlink:href="https://godoc.org/github.com/prometheus/common/expfmt" xlink:title="github.com/prometheus/common/expfmt" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3066,-6357.5998C3066,-6357.5998 2848,-6357.5998 2848,-6357.5998 2842,-6357.5998 2836,-6351.5998 2836,-6345.5998 2836,-6345.5998 2836,-6333.5998 2836,-6333.5998 2836,-6327.5998 2842,-6321.5998 2848,-6321.5998 2848,-6321.5998 3066,-6321.5998 3066,-6321.5998 3072,-6321.5998 3078,-6327.5998 3078,-6333.5998 3078,-6333.5998 3078,-6345.5998 3078,-6345.5998 3078,-6351.5998 3072,-6357.5998 3066,-6357.5998"/> +<text text-anchor="middle" x="2957" y="-6335.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/expfmt</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/expfmt --> +<g id="edge519" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/expfmt</title> +<path fill="none" stroke="#000000" d="M2588.0954,-6920.3459C2643.5728,-6833.0108 2881.7327,-6458.0889 2942.726,-6362.0705"/> +<polygon fill="#000000" stroke="#000000" points="2944.2913,-6362.8701 2945.4951,-6357.7113 2941.337,-6360.9934 2944.2913,-6362.8701"/> +</g> +<!-- github.com/prometheus/common/model --> +<g id="node126" class="node"> +<title>github.com/prometheus/common/model</title> +<g id="a_node126"><a xlink:href="https://godoc.org/github.com/prometheus/common/model" xlink:title="github.com/prometheus/common/model" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3888,-4944.5998C3888,-4944.5998 3675,-4944.5998 3675,-4944.5998 3669,-4944.5998 3663,-4938.5998 3663,-4932.5998 3663,-4932.5998 3663,-4920.5998 3663,-4920.5998 3663,-4914.5998 3669,-4908.5998 3675,-4908.5998 3675,-4908.5998 3888,-4908.5998 3888,-4908.5998 3894,-4908.5998 3900,-4914.5998 3900,-4920.5998 3900,-4920.5998 3900,-4932.5998 3900,-4932.5998 3900,-4938.5998 3894,-4944.5998 3888,-4944.5998"/> +<text text-anchor="middle" x="3781.5" y="-4922.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/model</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/model --> +<g id="edge520" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/model</title> +<path fill="none" stroke="#000000" d="M2579.0455,-6920.3223C2594.3811,-6812.4436 2677.4271,-6259.2768 2785,-6131.5998 2893.0492,-6003.3576 3032.4567,-6113.7132 3129,-5976.5998 3222.4333,-5843.9032 3087.5214,-5376.8269 3187,-5248.5998 3283.4916,-5124.2229 3409.5189,-5243.7281 3522,-5133.5998 3575.4506,-5081.2673 3524.1962,-5025.4154 3580,-4975.5998 3601.7074,-4956.2218 3629.7932,-4944.1956 3658.066,-4936.8105"/> +<polygon fill="#000000" stroke="#000000" points="3658.5522,-4938.4928 3662.9736,-4935.5748 3657.6976,-4935.0987 3658.5522,-4938.4928"/> +</g> +<!-- github.com/prometheus/procfs --> +<g id="node127" class="node"> +<title>github.com/prometheus/procfs</title> +<g id="a_node127"><a xlink:href="https://godoc.org/github.com/prometheus/procfs" xlink:title="github.com/prometheus/procfs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3434.5,-13812.5998C3434.5,-13812.5998 3274.5,-13812.5998 3274.5,-13812.5998 3268.5,-13812.5998 3262.5,-13806.5998 3262.5,-13800.5998 3262.5,-13800.5998 3262.5,-13788.5998 3262.5,-13788.5998 3262.5,-13782.5998 3268.5,-13776.5998 3274.5,-13776.5998 3274.5,-13776.5998 3434.5,-13776.5998 3434.5,-13776.5998 3440.5,-13776.5998 3446.5,-13782.5998 3446.5,-13788.5998 3446.5,-13788.5998 3446.5,-13800.5998 3446.5,-13800.5998 3446.5,-13806.5998 3440.5,-13812.5998 3434.5,-13812.5998"/> +<text text-anchor="middle" x="3354.5" y="-13790.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/procfs --> +<g id="edge521" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/procfs</title> +<path fill="none" stroke="#000000" d="M2578.5598,-6956.7942C2593.9698,-7093.8383 2691.6056,-7978.4425 2727,-8700.5998 2739.3835,-8953.2615 2696.2219,-13016.7249 2785,-13253.5998 2881.8235,-13511.9414 2963.4256,-13564.9531 3187,-13726.5998 3216.3325,-13747.8075 3253.2313,-13763.7079 3284.9259,-13774.7786"/> +<polygon fill="#000000" stroke="#000000" points="3284.6686,-13776.5408 3289.966,-13776.514 3285.8081,-13773.2315 3284.6686,-13776.5408"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->bufio --> +<g id="edge536" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->bufio</title> +<path fill="none" stroke="#000000" d="M2196.4555,-6499.7338C2207.9614,-6718.8792 2320.3459,-8881.1795 2368,-10630.5998 2371.3972,-10755.314 2372.3384,-15008.9695 2426,-15121.5998 2608.2176,-15504.0559 2788.2302,-15552.5663 3187,-15695.5998 3520.3259,-15815.1595 3728.4885,-15976.8205 3983,-15730.5998 4122.9069,-15595.2504 3942.2989,-14147.3839 4041,-13979.5998 4046.3373,-13970.5268 4055.2324,-13963.6547 4064.33,-13958.5952"/> +<polygon fill="#000000" stroke="#000000" points="4065.1803,-13960.1255 4068.7977,-13956.2554 4063.5564,-13957.0249 4065.1803,-13960.1255"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->compress/gzip --> +<g id="edge537" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->compress/gzip</title> +<path fill="none" stroke="#000000" d="M2197.4501,-6463.4768C2210.4483,-6346.0325 2288.2629,-5693.3825 2426,-5559.5998 2451.5901,-5534.7444 2491.3627,-5526.8301 2523.5958,-5524.9514"/> +<polygon fill="#000000" stroke="#000000" points="2523.7931,-5526.6939 2528.701,-5524.7001 2523.6209,-5523.1982 2523.7931,-5526.6939"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->crypto/tls --> +<g id="edge538" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->crypto/tls</title> +<path fill="none" stroke="#000000" d="M2196.4801,-6499.7331C2208.2778,-6718.8708 2323.4023,-8881.099 2368,-10630.5998 2385.4342,-11314.5191 2368.3684,-16105.8901 2426,-16787.5998 2497.0613,-17628.1655 2630.7522,-17823.5445 2727,-18661.5998 2737.5985,-18753.8837 2734.6682,-19417.5271 2785,-19495.5998 2934.8485,-19728.0391 3776.1392,-20016.1515 3983,-19832.5998 4064.0263,-19760.7036 4004.4541,-19450.5739 4041,-19348.5998 4049.4335,-19325.0679 4065.2697,-19301.3602 4077.7914,-19284.8138"/> +<polygon fill="#000000" stroke="#000000" points="4079.2419,-19285.7981 4080.8969,-19280.7658 4076.465,-19283.6677 4079.2419,-19285.7981"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->errors --> +<g id="edge539" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->errors</title> +<path fill="none" stroke="#000000" d="M2199.5965,-6463.5112C2220.9833,-6370.768 2322.7818,-5949.9851 2426,-5873.5998 2495.6139,-5822.083 3918.3669,-5739.9575 3983,-5797.5998 4057.3549,-5863.9123 4089.643,-6615.9268 4095.1377,-6759.126"/> +<polygon fill="#000000" stroke="#000000" points="4093.3973,-6759.4155 4095.3364,-6764.3453 4096.8948,-6759.2823 4093.3973,-6759.4155"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->fmt --> +<g id="edge540" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->fmt</title> +<path fill="none" stroke="#000000" d="M2197.8064,-6500.0055C2215.3774,-6632.6267 2339.6987,-7449.2125 2785,-7876.5998 2902.8055,-7989.6661 3010.3959,-7893.3716 3129,-8005.5998 3175.5453,-8049.643 3139.2023,-8094.919 3187,-8137.5998 3303.2854,-8241.4367 3385.6548,-8166.0062 3522,-8241.5998 3551.7654,-8258.1026 3549.3029,-8276.9026 3580,-8291.5998 3744.2504,-8370.2399 3847.3269,-8244.1285 3983,-8365.5998 4037.3033,-8414.2189 4079.7401,-8644.023 4092.2378,-8718.3489"/> +<polygon fill="#000000" stroke="#000000" points="4090.536,-8718.7836 4093.0856,-8723.4271 4093.9882,-8718.2071 4090.536,-8718.7836"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->io --> +<g id="edge544" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->io</title> +<path fill="none" stroke="#000000" d="M2196.3135,-6499.7384C2206.1328,-6718.9382 2302.6814,-8881.75 2368,-10630.5998 2371.6327,-10727.8631 2383.3628,-12294.1046 2426,-12381.5998 2502.4298,-12538.4404 2650.7091,-12476.6916 2727,-12633.5998 2816.9269,-12818.5532 2646.0851,-14331.9513 2785,-14483.5998 2965.3897,-14680.5249 3786.1167,-14759.0352 3983,-14578.5998 4092.1967,-14478.5256 3962.4787,-14040.1911 4041,-13914.5998 4046.4291,-13905.9161 4055.0559,-13899.2127 4063.882,-13894.1874"/> +<polygon fill="#000000" stroke="#000000" points="4065.2018,-13895.4663 4068.7869,-13891.5662 4063.5522,-13892.3794 4065.2018,-13895.4663"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->strconv --> +<g id="edge548" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->strconv</title> +<path fill="none" stroke="#000000" d="M2195.8046,-6463.151C2199.3021,-6319.2865 2242.8811,-5373.4422 2785,-5024.5998 3011.4458,-4878.8866 3772.9703,-4674.0841 3983,-4842.5998 4002.6381,-4858.3563 4073.5119,-5231.2868 4091.7147,-5328.5661"/> +<polygon fill="#000000" stroke="#000000" points="4090.0016,-5328.9262 4092.6406,-5333.5195 4093.442,-5328.2831 4090.0016,-5328.9262"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->strings --> +<g id="edge549" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->strings</title> +<path fill="none" stroke="#000000" d="M2220.5213,-6463.4097C2298.8458,-6407.74 2548.2325,-6240.0434 2785,-6183.5998 2933.7213,-6148.1458 2977.4539,-6163.3815 3129,-6183.5998 3226.4644,-6196.6029 3917.8125,-6341.9861 3983,-6415.5998 4042.5461,-6482.8428 4025.3214,-6727.1604 4041,-6815.5998 4056.7412,-6904.3922 4080.2146,-7009.0892 4090.6578,-7054.5791"/> +<polygon fill="#000000" stroke="#000000" points="4088.9795,-7055.0901 4091.8059,-7059.5705 4092.3904,-7054.3054 4088.9795,-7055.0901"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->sync --> +<g id="edge550" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->sync</title> +<path fill="none" stroke="#000000" d="M2199.0673,-6463.4163C2220.1712,-6354.889 2329.0729,-5781.3665 2368,-5307.5998 2375.3007,-5218.7458 2362.3919,-2161.0686 2426,-2098.5998 2487.7149,-2037.9903 3919.2782,-2040.1038 3983,-2098.5998 4038.6168,-2149.6555 4032.7924,-2700.5495 4041,-2775.5998 4057.2916,-2924.571 4083.2357,-3102.9113 4092.5392,-3165.5186"/> +<polygon fill="#000000" stroke="#000000" points="4090.8166,-3165.8333 4093.2838,-3170.5212 4094.2785,-3165.318 4090.8166,-3165.8333"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->time --> +<g id="edge551" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->time</title> +<path fill="none" stroke="#000000" d="M2196.8675,-6499.7749C2212.6947,-6709.3878 2361.7571,-8663.4365 2426,-8765.5998 2509.4651,-8898.3314 2647.1225,-8814.6788 2727,-8949.5998 2866.8554,-9185.8294 2632.8056,-9955.1248 2785,-10183.5998 2878.7942,-10324.4041 3011.4204,-10224.9517 3129,-10346.5998 3174.1008,-10393.2612 3136.9441,-10439.299 3187,-10480.5998 3304.6121,-10577.6409 3415.9449,-10445.0466 3522,-10554.5998 3619.4932,-10655.3087 3480.7196,-10765.6523 3580,-10864.5998 3708.9842,-10993.1515 3857.0128,-10807.1095 3983,-10938.5998 4111.8454,-11073.0733 3937.1751,-11198.9885 4041,-11353.5998 4046.6121,-11361.9571 4055.1976,-11368.4193 4063.9287,-11373.2766"/> +<polygon fill="#000000" stroke="#000000" points="4063.5347,-11375.0453 4068.7764,-11375.8118 4065.1567,-11371.9439 4063.5347,-11375.0453"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net/http --> +<g id="edge546" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net/http</title> +<path fill="none" stroke="#000000" d="M2196.2582,-6463.4958C2209.4062,-6149.9091 2389.5871,-1866.1787 2426,-1744.5998 2516.0354,-1443.9811 2614.1036,-1399.7964 2785,-1136.5998 2805.9791,-1104.2901 3155.9809,-594.444 3187,-571.5998 3475.5797,-359.0732 3659.1005,-289.1888 3983,-442.5998 4033.4129,-466.4773 4068.8301,-525.0826 4085.3934,-557.7184"/> +<polygon fill="#000000" stroke="#000000" points="4083.9006,-558.6463 4087.6999,-562.3379 4087.032,-557.0827 4083.9006,-558.6463"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net --> +<g id="edge545" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net</title> +<path fill="none" stroke="#000000" d="M2196.4792,-6499.7331C2208.2671,-6718.8711 2323.2994,-8881.1016 2368,-10630.5998 2370.1574,-10715.0344 2377.0677,-16642.7559 2426,-16711.5998 2508.3407,-16827.4467 2618.782,-16727.462 2727,-16819.5998 2767.593,-16854.1611 2744.8322,-16889.5453 2785,-16924.5998 2908.5015,-17032.3797 2988.0035,-16973.9988 3129,-17057.5998 3158.0268,-17074.8107 3156.7936,-17091.5552 3187,-17106.5998 3347.3068,-17186.4423 3402.0196,-17175.6988 3580,-17195.5998 3758.0018,-17215.5032 3804.7908,-17213.5521 3983,-17195.5998 4010.5932,-17192.8202 4041.4107,-17186.3144 4063.8839,-17180.9288"/> +<polygon fill="#000000" stroke="#000000" points="4064.388,-17182.6073 4068.8347,-17179.7282 4063.563,-17179.2059 4064.388,-17182.6073"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_golang/prometheus --> +<g id="edge541" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_golang/prometheus</title> +<path fill="none" stroke="#000000" d="M2210.6708,-6499.7968C2270.7688,-6571.8828 2492.0631,-6837.3198 2557.9029,-6916.293"/> +<polygon fill="#000000" stroke="#000000" points="2556.74,-6917.6311 2561.2859,-6920.3509 2559.4283,-6915.3898 2556.74,-6917.6311"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_model/go --> +<g id="edge542" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M2368.0751,-6489.9382C2604.0746,-6501.3411 3021.8591,-6521.5274 3229.7624,-6531.5728"/> +<polygon fill="#000000" stroke="#000000" points="3229.86,-6533.3295 3234.9387,-6531.8229 3230.029,-6529.8336 3229.86,-6533.3295"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/common/expfmt --> +<g id="edge543" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/common/expfmt</title> +<path fill="none" stroke="#000000" d="M2292.3309,-6463.5434C2436.8317,-6436.5977 2707.6432,-6386.0984 2855.0583,-6358.6093"/> +<polygon fill="#000000" stroke="#000000" points="2855.6554,-6360.2782 2860.2499,-6357.6412 2855.0138,-6356.8375 2855.6554,-6360.2782"/> +</g> +<!-- net/http/httptrace --> +<g id="node128" class="node"> +<title>net/http/httptrace</title> +<g id="a_node128"><a xlink:href="https://godoc.org/net/http/httptrace" xlink:title="net/http/httptrace" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2619,-5609.5998C2619,-5609.5998 2534,-5609.5998 2534,-5609.5998 2528,-5609.5998 2522,-5603.5998 2522,-5597.5998 2522,-5597.5998 2522,-5585.5998 2522,-5585.5998 2522,-5579.5998 2528,-5573.5998 2534,-5573.5998 2534,-5573.5998 2619,-5573.5998 2619,-5573.5998 2625,-5573.5998 2631,-5579.5998 2631,-5585.5998 2631,-5585.5998 2631,-5597.5998 2631,-5597.5998 2631,-5603.5998 2625,-5609.5998 2619,-5609.5998"/> +<text text-anchor="middle" x="2576.5" y="-5587.8998" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httptrace</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net/http/httptrace --> +<g id="edge547" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net/http/httptrace</title> +<path fill="none" stroke="#000000" d="M2197.9321,-6463.5273C2213.3064,-6351.8666 2300.4057,-5756.5636 2426,-5631.5998 2449.7333,-5607.9857 2485.7222,-5597.8575 2516.5562,-5593.6938"/> +<polygon fill="#000000" stroke="#000000" points="2517.1091,-5595.3882 2521.8524,-5593.0296 2516.6735,-5591.9154 2517.1091,-5595.3882"/> +</g> +<!-- gopkg.in/yaml.v2->bytes --> +<g id="edge673" class="edge"> +<title>gopkg.in/yaml.v2->bytes</title> +<path fill="none" stroke="#000000" d="M3838.0368,-4386.4208C3885.7053,-4397.1578 3951.1052,-4420.3852 3983,-4469.5998 4040.9974,-4559.0917 4038.7146,-12040.9824 4041,-12147.5998 4054.4058,-12773.0144 4088.4789,-13539.1817 4094.9352,-13681.3376"/> +<polygon fill="#000000" stroke="#000000" points="4093.195,-13681.5949 4095.1705,-13686.5102 4096.6914,-13681.4358 4093.195,-13681.5949"/> +</g> +<!-- gopkg.in/yaml.v2->encoding --> +<g id="edge674" class="edge"> +<title>gopkg.in/yaml.v2->encoding</title> +<path fill="none" stroke="#000000" d="M3838.0541,-4374.4155C3882.5228,-4374.986 3943.3796,-4382.9928 3983,-4417.5998 4030.9875,-4459.5152 4076.6367,-4655.8426 4091.2016,-4723.5646"/> +<polygon fill="#000000" stroke="#000000" points="4089.5194,-4724.067 4092.2761,-4728.5906 4092.942,-4723.3352 4089.5194,-4724.067"/> +</g> +<!-- gopkg.in/yaml.v2->encoding/base64 --> +<g id="edge675" class="edge"> +<title>gopkg.in/yaml.v2->encoding/base64</title> +<path fill="none" stroke="#000000" d="M3838.0716,-4386.3983C3885.7627,-4397.1207 3951.1761,-4420.3393 3983,-4469.5998 4076.5391,-4614.3901 3954.7124,-16731.3744 4041,-16880.5998 4043.2655,-16884.5179 4046.2146,-16888.0014 4049.5703,-16891.0892"/> +<polygon fill="#000000" stroke="#000000" points="4048.7239,-16892.6632 4053.6798,-16894.5345 4050.9725,-16889.9811 4048.7239,-16892.6632"/> +</g> +<!-- gopkg.in/yaml.v2->errors --> +<g id="edge676" class="edge"> +<title>gopkg.in/yaml.v2->errors</title> +<path fill="none" stroke="#000000" d="M3838.0086,-4386.8567C3885.3171,-4397.8517 3950.2046,-4421.2128 3983,-4469.5998 3999.9771,-4494.6482 4085.0291,-6519.8657 4095.0377,-6759.5137"/> +<polygon fill="#000000" stroke="#000000" points="4093.2898,-6759.6025 4095.2469,-6764.5251 4096.7867,-6759.4564 4093.2898,-6759.6025"/> +</g> +<!-- gopkg.in/yaml.v2->fmt --> +<g id="edge677" class="edge"> +<title>gopkg.in/yaml.v2->fmt</title> +<path fill="none" stroke="#000000" d="M3838.3991,-4386.5966C3885.958,-4397.425 3950.9933,-4420.6875 3983,-4469.5998 4036.8197,-4551.8467 4036.5203,-7909.411 4041,-8007.5998 4053.7912,-8287.9621 4085.1155,-8627.5043 4093.759,-8718.3349"/> +<polygon fill="#000000" stroke="#000000" points="4092.0314,-8718.6533 4094.2482,-8723.4645 4095.5156,-8718.3209 4092.0314,-8718.6533"/> +</g> +<!-- gopkg.in/yaml.v2->io --> +<g id="edge678" class="edge"> +<title>gopkg.in/yaml.v2->io</title> +<path fill="none" stroke="#000000" d="M3838.0524,-4386.4107C3885.7311,-4397.1411 3951.1371,-4420.3645 3983,-4469.5998 4052.9372,-4577.6685 4017.2518,-13611.0847 4041,-13737.5998 4049.4009,-13782.3545 4070.9326,-13831.2325 4084.4282,-13858.9803"/> +<polygon fill="#000000" stroke="#000000" points="4082.9114,-13859.8618 4086.6857,-13863.579 4086.0533,-13858.3195 4082.9114,-13859.8618"/> +</g> +<!-- gopkg.in/yaml.v2->math --> +<g id="edge679" class="edge"> +<title>gopkg.in/yaml.v2->math</title> +<path fill="none" stroke="#000000" d="M3792.1442,-4359.5267C3839.4219,-4279.2523 4030.3615,-3955.0497 4082.6846,-3866.2085"/> +<polygon fill="#000000" stroke="#000000" points="4084.3289,-3866.8649 4085.3585,-3861.6684 4081.3131,-3865.0887 4084.3289,-3866.8649"/> +</g> +<!-- gopkg.in/yaml.v2->reflect --> +<g id="edge680" class="edge"> +<title>gopkg.in/yaml.v2->reflect</title> +<path fill="none" stroke="#000000" d="M3838.3535,-4372.6666C3883.9161,-4372.0606 3946.1268,-4379.3329 3983,-4417.5998 4039.53,-4476.2666 4025.4477,-4699.6276 4041,-4779.5998 4056.5652,-4859.6382 4079.4698,-4953.6455 4090.1352,-4996.3698"/> +<polygon fill="#000000" stroke="#000000" points="4088.4627,-4996.8956 4091.3738,-5001.3214 4091.8581,-4996.0462 4088.4627,-4996.8956"/> +</g> +<!-- gopkg.in/yaml.v2->sort --> +<g id="edge682" class="edge"> +<title>gopkg.in/yaml.v2->sort</title> +<path fill="none" stroke="#000000" d="M3838.0231,-4388.479C3878.4512,-4396.3436 3934.133,-4407.3451 3983,-4417.5998 4010.2394,-4423.316 4041.086,-4430.166 4063.6604,-4435.249"/> +<polygon fill="#000000" stroke="#000000" points="4063.3728,-4436.978 4068.6353,-4436.3708 4064.1427,-4433.5638 4063.3728,-4436.978"/> +</g> +<!-- gopkg.in/yaml.v2->strconv --> +<g id="edge683" class="edge"> +<title>gopkg.in/yaml.v2->strconv</title> +<path fill="none" stroke="#000000" d="M3838.1208,-4372.0443C3884.1786,-4370.9411 3947.2061,-4377.8403 3983,-4417.5998 4077.8065,-4522.9098 4021.7182,-4912.2193 4041,-5052.5998 4055.2346,-5156.2345 4080.3941,-5278.6157 4091.0308,-5328.6006"/> +<polygon fill="#000000" stroke="#000000" points="4089.3192,-5328.966 4092.0742,-5333.4907 4092.7422,-5328.2356 4089.3192,-5328.966"/> +</g> +<!-- gopkg.in/yaml.v2->strings --> +<g id="edge684" class="edge"> +<title>gopkg.in/yaml.v2->strings</title> +<path fill="none" stroke="#000000" d="M3838.2984,-4386.6628C3885.7927,-4397.5336 3950.79,-4420.8211 3983,-4469.5998 4054.84,-4578.394 4025.9341,-6686.1001 4041,-6815.5998 4051.395,-6904.9503 4077.5084,-7008.9495 4089.632,-7054.3784"/> +<polygon fill="#000000" stroke="#000000" points="4087.9841,-7054.9897 4090.9697,-7059.3656 4091.3646,-7054.0829 4087.9841,-7054.9897"/> +</g> +<!-- gopkg.in/yaml.v2->sync --> +<g id="edge685" class="edge"> +<title>gopkg.in/yaml.v2->sync</title> +<path fill="none" stroke="#000000" d="M3786.6154,-4359.3273C3809.8315,-4276.2925 3907.4198,-3926.1204 3983,-3637.5998 4025.583,-3475.0429 4073.9735,-3278.5582 4090.2895,-3211.9607"/> +<polygon fill="#000000" stroke="#000000" points="4092.0091,-3212.2958 4091.4987,-3207.023 4088.6095,-3211.4632 4092.0091,-3212.2958"/> +</g> +<!-- gopkg.in/yaml.v2->time --> +<g id="edge686" class="edge"> +<title>gopkg.in/yaml.v2->time</title> +<path fill="none" stroke="#000000" d="M3838.0263,-4386.4276C3885.688,-4397.1691 3951.0838,-4420.3991 3983,-4469.5998 4087.0702,-4630.0303 3944.9651,-11188.2341 4041,-11353.5998 4046.2922,-11362.7126 4055.2769,-11369.4772 4064.4675,-11374.3783"/> +<polygon fill="#000000" stroke="#000000" points="4063.726,-11375.9641 4068.9806,-11376.6368 4065.2924,-11372.8341 4063.726,-11375.9641"/> +</g> +<!-- gopkg.in/yaml.v2->unicode --> +<g id="edge687" class="edge"> +<title>gopkg.in/yaml.v2->unicode</title> +<path fill="none" stroke="#000000" d="M3805.1386,-4359.5608C3862.0565,-4316.1259 4006.5311,-4205.875 4067.8204,-4159.1042"/> +<polygon fill="#000000" stroke="#000000" points="4069.1739,-4160.2727 4072.0871,-4155.8482 4067.0506,-4157.4903 4069.1739,-4160.2727"/> +</g> +<!-- gopkg.in/yaml.v2->unicode/utf8 --> +<g id="edge688" class="edge"> +<title>gopkg.in/yaml.v2->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3838.0895,-4377.5998C3898.1732,-4377.5998 3992.0811,-4377.5998 4048.4679,-4377.5998"/> +<polygon fill="#000000" stroke="#000000" points="4048.4689,-4379.3499 4053.4689,-4377.5998 4048.4689,-4375.8499 4048.4689,-4379.3499"/> +</g> +<!-- gopkg.in/yaml.v2->regexp --> +<g id="edge681" class="edge"> +<title>gopkg.in/yaml.v2->regexp</title> +<path fill="none" stroke="#000000" d="M3787.8733,-4359.5957C3816.5404,-4277.7237 3934.2553,-3931.8454 3983,-3637.5998 4050.4658,-3230.3445 4000.8401,-3121.4473 4041,-2710.5998 4055.5791,-2561.4513 4082.5825,-2383.2207 4092.3523,-2320.6616"/> +<polygon fill="#000000" stroke="#000000" points="4094.0904,-2320.8736 4093.1347,-2315.663 4090.6325,-2320.3322 4094.0904,-2320.8736"/> +</g> +<!-- github.com/golang/protobuf/proto->bufio --> +<g id="edge394" class="edge"> +<title>github.com/golang/protobuf/proto->bufio</title> +<path fill="none" stroke="#000000" d="M3786.6116,-6620.6949C3813.7264,-6717.7138 3941.069,-7186.5285 3983,-7579.5998 4001.6668,-7754.5866 3952.5678,-13762.4533 4041,-13914.5998 4046.2955,-13923.7106 4055.2811,-13930.4748 4064.4715,-13935.376"/> +<polygon fill="#000000" stroke="#000000" points="4063.7298,-13936.9617 4068.9843,-13937.6346 4065.2963,-13933.8318 4063.7298,-13936.9617"/> +</g> +<!-- github.com/golang/protobuf/proto->bytes --> +<g id="edge395" class="edge"> +<title>github.com/golang/protobuf/proto->bytes</title> +<path fill="none" stroke="#000000" d="M3786.5871,-6620.6975C3813.5755,-6717.73 3940.3731,-7186.6033 3983,-7579.5998 4037.7361,-8084.2364 4028.8173,-11640.1496 4041,-12147.5998 4056.0138,-12772.9778 4088.7895,-13539.1746 4094.9802,-13681.3365"/> +<polygon fill="#000000" stroke="#000000" points="4093.2395,-13681.5904 4095.2057,-13686.5094 4096.7362,-13681.4378 4093.2395,-13681.5904"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding --> +<g id="edge396" class="edge"> +<title>github.com/golang/protobuf/proto->encoding</title> +<path fill="none" stroke="#000000" d="M3787.8478,-6584.5915C3816.4042,-6502.701 3933.7106,-6156.7547 3983,-5862.5998 4047.4083,-5478.2164 3984.5768,-5373.2363 4041,-4987.5998 4052.851,-4906.6016 4077.5763,-4812.5096 4089.4112,-4769.8046"/> +<polygon fill="#000000" stroke="#000000" points="4091.1338,-4770.1419 4090.7888,-4764.8557 4087.762,-4769.2032 4091.1338,-4770.1419"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding/json --> +<g id="edge397" class="edge"> +<title>github.com/golang/protobuf/proto->encoding/json</title> +<path fill="none" stroke="#000000" d="M3786.6333,-6620.6926C3813.8598,-6717.6996 3941.6842,-7186.4633 3983,-7579.5998 3996.9736,-7712.5644 3973.9964,-17089.9046 4041,-17205.5998 4043.2682,-17209.5163 4046.219,-17212.9988 4049.5759,-17216.0859"/> +<polygon fill="#000000" stroke="#000000" points="4048.7302,-17217.6604 4053.6865,-17219.5306 4050.9782,-17214.9778 4048.7302,-17217.6604"/> +</g> +<!-- github.com/golang/protobuf/proto->errors --> +<g id="edge398" class="edge"> +<title>github.com/golang/protobuf/proto->errors</title> +<path fill="none" stroke="#000000" d="M3882.5842,-6608.7929C3916.3652,-6614.3628 3953.0143,-6624.4687 3983,-6642.5998 4030.6398,-6671.4057 4066.7181,-6727.9633 4084.2413,-6759.6665"/> +<polygon fill="#000000" stroke="#000000" points="4082.7603,-6760.6061 4086.6912,-6764.1574 4085.8329,-6758.9299 4082.7603,-6760.6061"/> +</g> +<!-- github.com/golang/protobuf/proto->fmt --> +<g id="edge399" class="edge"> +<title>github.com/golang/protobuf/proto->fmt</title> +<path fill="none" stroke="#000000" d="M3786.005,-6620.7731C3809.9959,-6718.1947 3923.8618,-7188.747 3983,-7579.5998 4051.9446,-8035.2644 4087.5593,-8598.1453 4094.6631,-8718.3474"/> +<polygon fill="#000000" stroke="#000000" points="4092.9204,-8718.5243 4094.961,-8723.4129 4096.4143,-8718.3187 4092.9204,-8718.5243"/> +</g> +<!-- github.com/golang/protobuf/proto->io --> +<g id="edge400" class="edge"> +<title>github.com/golang/protobuf/proto->io</title> +<path fill="none" stroke="#000000" d="M3786.6098,-6620.6951C3813.7152,-6717.715 3941.0172,-7186.534 3983,-7579.5998 4055.6706,-8259.9825 3913.7007,-13065.293 4041,-13737.5998 4049.4716,-13782.3411 4070.98,-13831.2235 4084.4521,-13858.9758"/> +<polygon fill="#000000" stroke="#000000" points="4082.9341,-13859.8551 4086.7054,-13863.5753 4086.0771,-13858.3152 4082.9341,-13859.8551"/> +</g> +<!-- github.com/golang/protobuf/proto->math --> +<g id="edge402" class="edge"> +<title>github.com/golang/protobuf/proto->math</title> +<path fill="none" stroke="#000000" d="M3788.1762,-6584.3533C3817.8962,-6502.1245 3938.6483,-6157.2029 3983,-5862.5998 4099.3142,-5089.9919 3947.499,-4881.2992 4041,-4105.5998 4051.7648,-4016.2931 4077.6889,-3912.2715 4089.6981,-3866.8291"/> +<polygon fill="#000000" stroke="#000000" points="4091.4309,-3867.122 4091.0229,-3861.8403 4088.0481,-3866.2236 4091.4309,-3867.122"/> +</g> +<!-- github.com/golang/protobuf/proto->reflect --> +<g id="edge403" class="edge"> +<title>github.com/golang/protobuf/proto->reflect</title> +<path fill="none" stroke="#000000" d="M3787.571,-6584.5422C3814.9279,-6502.4384 3927.8053,-6155.704 3983,-5862.5998 4027.9148,-5624.0858 4006.2034,-5559.7986 4041,-5319.5998 4056.0461,-5215.7379 4080.7606,-5092.8819 4091.1549,-5042.6942"/> +<polygon fill="#000000" stroke="#000000" points="4092.8713,-5043.0356 4092.1742,-5037.7842 4089.4443,-5042.3241 4092.8713,-5043.0356"/> +</g> +<!-- github.com/golang/protobuf/proto->sort --> +<g id="edge404" class="edge"> +<title>github.com/golang/protobuf/proto->sort</title> +<path fill="none" stroke="#000000" d="M3788.0537,-6584.3343C3817.2478,-6502.024 3936.075,-6156.804 3983,-5862.5998 4063.4661,-5358.1036 3977.3082,-5221.487 4041,-4714.5998 4052.7065,-4621.4344 4078.4668,-4512.4725 4090.0967,-4465.7662"/> +<polygon fill="#000000" stroke="#000000" points="4091.7952,-4466.1876 4091.3104,-4460.9125 4088.3997,-4465.3386 4091.7952,-4466.1876"/> +</g> +<!-- github.com/golang/protobuf/proto->strconv --> +<g id="edge405" class="edge"> +<title>github.com/golang/protobuf/proto->strconv</title> +<path fill="none" stroke="#000000" d="M3882.6715,-6606.3204C3919.329,-6602.1884 3957.8573,-6590.4732 3983,-6562.5998 4065.6418,-6470.9825 4091.5731,-5537.2023 4095.4595,-5375.4217"/> +<polygon fill="#000000" stroke="#000000" points="4097.2191,-5375.0352 4095.5885,-5369.995 4093.7201,-5374.9519 4097.2191,-5375.0352"/> +</g> +<!-- github.com/golang/protobuf/proto->strings --> +<g id="edge406" class="edge"> +<title>github.com/golang/protobuf/proto->strings</title> +<path fill="none" stroke="#000000" d="M3882.7583,-6601.3712C3918.4419,-6605.9919 3956.2796,-6617.3907 3983,-6642.5998 4014.027,-6671.8719 4074.4969,-6968.5271 4091.472,-7054.4176"/> +<polygon fill="#000000" stroke="#000000" points="4089.796,-7054.9641 4092.4804,-7059.531 4093.2298,-7054.2869 4089.796,-7054.9641"/> +</g> +<!-- github.com/golang/protobuf/proto->sync --> +<g id="edge407" class="edge"> +<title>github.com/golang/protobuf/proto->sync</title> +<path fill="none" stroke="#000000" d="M3788.2093,-6584.3582C3818.0717,-6502.1508 3939.3447,-6157.3069 3983,-5862.5998 4049.8128,-5411.5615 4014.495,-4266.7888 4041,-3811.5998 4054.6648,-3576.9245 4084.5527,-3293.6101 4093.4514,-3211.7615"/> +<polygon fill="#000000" stroke="#000000" points="4095.2048,-3211.8244 4094.0068,-3206.6643 4091.7254,-3211.4453 4095.2048,-3211.8244"/> +</g> +<!-- github.com/golang/protobuf/proto->unicode/utf8 --> +<g id="edge409" class="edge"> +<title>github.com/golang/protobuf/proto->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3788.1276,-6584.3459C3817.6392,-6502.0854 3937.6285,-6157.0476 3983,-5862.5998 4007.5895,-5703.0212 3958.6592,-4549.4881 4041,-4410.5998 4043.598,-4406.2176 4047.024,-4402.3477 4050.8991,-4398.947"/> +<polygon fill="#000000" stroke="#000000" points="4052.1241,-4400.207 4054.9098,-4395.7011 4049.9223,-4397.4863 4052.1241,-4400.207"/> +</g> +<!-- github.com/golang/protobuf/proto->unsafe --> +<g id="edge410" class="edge"> +<title>github.com/golang/protobuf/proto->unsafe</title> +<path fill="none" stroke="#000000" d="M3786.6123,-6620.6948C3813.7303,-6717.7133 3941.0872,-7186.5265 3983,-7579.5998 4001.8501,-7756.3828 3951.6677,-13825.8882 4041,-13979.5998 4046.2951,-13988.7109 4055.2805,-13995.4751 4064.471,-14000.3763"/> +<polygon fill="#000000" stroke="#000000" points="4063.7293,-14001.962 4068.9838,-14002.6349 4065.2958,-13998.8321 4063.7293,-14001.962"/> +</g> +<!-- github.com/golang/protobuf/proto->log --> +<g id="edge401" class="edge"> +<title>github.com/golang/protobuf/proto->log</title> +<path fill="none" stroke="#000000" d="M3786.5461,-6620.702C3813.3233,-6717.7577 3939.2098,-7186.7313 3983,-7579.5998 4059.6954,-8267.6809 4004.236,-10003.2344 4041,-10694.5998 4053.5422,-10930.4627 4084.2063,-11215.1016 4093.3712,-11297.3307"/> +<polygon fill="#000000" stroke="#000000" points="4091.6489,-11297.6769 4093.9434,-11302.4516 4095.1273,-11297.2882 4091.6489,-11297.6769"/> +</g> +<!-- github.com/golang/protobuf/proto->sync/atomic --> +<g id="edge408" class="edge"> +<title>github.com/golang/protobuf/proto->sync/atomic</title> +<path fill="none" stroke="#000000" d="M3785.6496,-6620.7022C3819.6562,-6769.0553 4052.1659,-7783.3745 4090.619,-7951.1254"/> +<polygon fill="#000000" stroke="#000000" points="4088.9796,-7951.8059 4091.8025,-7956.2885 4092.3911,-7951.0239 4088.9796,-7951.8059"/> +</g> +<!-- github.com/klauspost/compress/flate->bufio --> +<g id="edge421" class="edge"> +<title>github.com/klauspost/compress/flate->bufio</title> +<path fill="none" stroke="#000000" d="M3890.0097,-10465.6524C3924.7177,-10469.0978 3960.0516,-10480.0485 3983,-10506.5998 4106.8249,-10649.8649 3945.2007,-13751.2597 4041,-13914.5998 4046.2832,-13923.6078 4055.1642,-13930.3266 4064.2654,-13935.2184"/> +<polygon fill="#000000" stroke="#000000" points="4063.484,-13936.7843 4068.7362,-13937.4751 4065.0612,-13933.6597 4063.484,-13936.7843"/> +</g> +<!-- github.com/klauspost/compress/flate->bytes --> +<g id="edge422" class="edge"> +<title>github.com/klauspost/compress/flate->bytes</title> +<path fill="none" stroke="#000000" d="M3890.2622,-10465.9437C3924.778,-10469.4673 3959.9267,-10480.3927 3983,-10506.5998 3983,-10506.5998 4084.8753,-13389.7624 4095.1646,-13680.9572"/> +<polygon fill="#000000" stroke="#000000" points="4093.425,-13681.284 4095.3505,-13686.2191 4096.9228,-13681.1604 4093.425,-13681.284"/> +</g> +<!-- github.com/klauspost/compress/flate->encoding/binary --> +<g id="edge423" class="edge"> +<title>github.com/klauspost/compress/flate->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3890.1514,-10466.0416C3924.6579,-10469.5734 3959.8291,-10480.4789 3983,-10506.5998 4033.0001,-10562.9657 4031.3506,-11786.8735 4041,-11861.5998 4052.0858,-11947.45 4077.6169,-12047.308 4089.5868,-12091.5168"/> +<polygon fill="#000000" stroke="#000000" points="4087.9075,-12092.0109 4090.9087,-12096.3762 4091.2848,-12091.0922 4087.9075,-12092.0109"/> +</g> +<!-- github.com/klauspost/compress/flate->fmt --> +<g id="edge424" class="edge"> +<title>github.com/klauspost/compress/flate->fmt</title> +<path fill="none" stroke="#000000" d="M3890.1361,-10476.7791C3924.6413,-10471.998 3959.8156,-10459.9934 3983,-10433.5998 4040.7474,-10367.8592 4088.7051,-8964.5942 4095.2413,-8765.1095"/> +<polygon fill="#000000" stroke="#000000" points="4096.9997,-8764.8787 4095.414,-8759.8242 4093.5016,-8764.7643 4096.9997,-8764.8787"/> +</g> +<!-- github.com/klauspost/compress/flate->io --> +<g id="edge425" class="edge"> +<title>github.com/klauspost/compress/flate->io</title> +<path fill="none" stroke="#000000" d="M3890.2587,-10465.6907C3924.8809,-10469.1663 3960.0972,-10480.1263 3983,-10506.5998 4041.7295,-10574.4857 4023.924,-13649.4745 4041,-13737.5998 4049.6624,-13782.3046 4071.1079,-13831.199 4084.5164,-13858.9635"/> +<polygon fill="#000000" stroke="#000000" points="4082.9953,-13859.837 4086.7588,-13863.5651 4086.1416,-13858.3037 4082.9953,-13859.837"/> +</g> +<!-- github.com/klauspost/compress/flate->math --> +<g id="edge426" class="edge"> +<title>github.com/klauspost/compress/flate->math</title> +<path fill="none" stroke="#000000" d="M3786.9664,-10455.4263C3814.797,-10361.8625 3941.0589,-9924.6343 3983,-9556.5998 4120.1633,-8352.9883 3909.4841,-5309.8415 4041,-4105.5998 4050.7658,-4016.1784 4077.2014,-3912.2155 4089.5197,-3866.8086"/> +<polygon fill="#000000" stroke="#000000" points="4091.2518,-3867.1081 4090.8792,-3861.8238 4087.8752,-3866.1871 4091.2518,-3867.1081"/> +</g> +<!-- github.com/klauspost/compress/flate->sort --> +<g id="edge428" class="edge"> +<title>github.com/klauspost/compress/flate->sort</title> +<path fill="none" stroke="#000000" d="M3786.9571,-10455.4253C3814.7417,-10361.8562 3940.8127,-9924.6062 3983,-9556.5998 4105.5562,-8487.5245 3927.4202,-5784.6661 4041,-4714.5998 4050.9109,-4621.2263 4077.6097,-4512.3731 4089.7899,-4465.7307"/> +<polygon fill="#000000" stroke="#000000" points="4091.4853,-4466.1641 4091.062,-4460.8837 4088.0999,-4465.2756 4091.4853,-4466.1641"/> +</g> +<!-- github.com/klauspost/compress/flate->strconv --> +<g id="edge429" class="edge"> +<title>github.com/klauspost/compress/flate->strconv</title> +<path fill="none" stroke="#000000" d="M3786.8966,-10455.4182C3814.3827,-10361.8143 3939.2142,-9924.4194 3983,-9556.5998 4056.7243,-8937.2838 4022.3044,-7374.0083 4041,-6750.5998 4057.7448,-6192.2418 4088.7494,-5508.9365 4094.917,-5374.9915"/> +<polygon fill="#000000" stroke="#000000" points="4096.6769,-5374.8153 4095.159,-5369.74 4093.1806,-5374.6541 4096.6769,-5374.8153"/> +</g> +<!-- github.com/klauspost/compress/flate->sync --> +<g id="edge430" class="edge"> +<title>github.com/klauspost/compress/flate->sync</title> +<path fill="none" stroke="#000000" d="M3786.9702,-10455.4267C3814.8195,-10361.8651 3941.1591,-9924.6457 3983,-9556.5998 4055.1075,-8922.3195 4009.6849,-4449.1971 4041,-3811.5998 4052.5315,-3576.81 4083.8944,-3293.5748 4093.299,-3211.7533"/> +<polygon fill="#000000" stroke="#000000" points="4095.0522,-3211.8253 4093.8863,-3206.6578 4091.5752,-3211.4245 4095.0522,-3211.8253"/> +</g> +<!-- github.com/klauspost/compress/flate->math/bits --> +<g id="edge427" class="edge"> +<title>github.com/klauspost/compress/flate->math/bits</title> +<path fill="none" stroke="#000000" d="M3890.2171,-10475.9489C3921.9394,-10480.5891 3955.4504,-10489.6487 3983,-10506.5998 4034.7687,-10538.4529 4070.1599,-10603.6779 4086.2111,-10638.4953"/> +<polygon fill="#000000" stroke="#000000" points="4084.7788,-10639.5741 4088.4386,-10643.4041 4087.966,-10638.1278 4084.7788,-10639.5741"/> +</g> +<!-- github.com/klauspost/compress/fse --> +<g id="node114" class="node"> +<title>github.com/klauspost/compress/fse</title> +<g id="a_node114"><a xlink:href="https://godoc.org/github.com/klauspost/compress/fse" xlink:title="github.com/klauspost/compress/fse" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3874,-10556.5998C3874,-10556.5998 3689,-10556.5998 3689,-10556.5998 3683,-10556.5998 3677,-10550.5998 3677,-10544.5998 3677,-10544.5998 3677,-10532.5998 3677,-10532.5998 3677,-10526.5998 3683,-10520.5998 3689,-10520.5998 3689,-10520.5998 3874,-10520.5998 3874,-10520.5998 3880,-10520.5998 3886,-10526.5998 3886,-10532.5998 3886,-10532.5998 3886,-10544.5998 3886,-10544.5998 3886,-10550.5998 3880,-10556.5998 3874,-10556.5998"/> +<text text-anchor="middle" x="3781.5" y="-10534.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/klauspost/compress/fse</text> +</a> +</g> +</g> +<!-- github.com/klauspost/compress/fse->errors --> +<g id="edge431" class="edge"> +<title>github.com/klauspost/compress/fse->errors</title> +<path fill="none" stroke="#000000" d="M3886.0213,-10547.4712C3921.97,-10544.6569 3959.1379,-10534.0808 3983,-10506.5998 4046.0413,-10433.9977 4030.3055,-7141.1556 4041,-7045.5998 4051.0416,-6955.878 4077.336,-6851.4973 4089.5689,-6805.9045"/> +<polygon fill="#000000" stroke="#000000" points="4091.3064,-6806.1825 4090.9189,-6800.8992 4087.9272,-6805.271 4091.3064,-6806.1825"/> +</g> +<!-- github.com/klauspost/compress/fse->fmt --> +<g id="edge432" class="edge"> +<title>github.com/klauspost/compress/fse->fmt</title> +<path fill="none" stroke="#000000" d="M3886.1173,-10546.7935C3921.7456,-10543.8203 3958.6752,-10533.3139 3983,-10506.5998 4044.5692,-10438.983 4089.4154,-8968.3422 4095.3344,-8764.872"/> +<polygon fill="#000000" stroke="#000000" points="4097.0869,-8764.8089 4095.4827,-8759.7602 4093.5884,-8764.7074 4097.0869,-8764.8089"/> +</g> +<!-- github.com/klauspost/compress/fse->io --> +<g id="edge433" class="edge"> +<title>github.com/klauspost/compress/fse->io</title> +<path fill="none" stroke="#000000" d="M3824.6922,-10556.6894C3873.2033,-10579.6303 3949.5195,-10624.2337 3983,-10689.5998 4060.2095,-10840.3408 4008.6922,-13571.3459 4041,-13737.5998 4049.6865,-13782.2999 4071.1241,-13831.1959 4084.5245,-13858.9619"/> +<polygon fill="#000000" stroke="#000000" points="4083.003,-13859.8346 4086.7655,-13863.5638 4086.1497,-13858.3022 4083.003,-13859.8346"/> +</g> +<!-- github.com/klauspost/compress/fse->math/bits --> +<g id="edge434" class="edge"> +<title>github.com/klauspost/compress/fse->math/bits</title> +<path fill="none" stroke="#000000" d="M3886.141,-10550.4394C3918.1251,-10556.3971 3952.8065,-10565.3063 3983,-10578.5998 4017.3949,-10593.7431 4051.4033,-10620.8093 4072.9815,-10639.9094"/> +<polygon fill="#000000" stroke="#000000" points="4071.9689,-10641.3515 4076.8632,-10643.3785 4074.3012,-10638.7418 4071.9689,-10641.3515"/> +</g> +<!-- github.com/klauspost/compress/huff0->errors --> +<g id="edge435" class="edge"> +<title>github.com/klauspost/compress/huff0->errors</title> +<path fill="none" stroke="#000000" d="M3373.9304,-10018.4444C3409.8041,-9983.6646 3486.0456,-9903.6043 3522,-9819.5998 3589.2239,-9662.5365 3468.5408,-9569.0787 3580,-9439.5998 3702.3441,-9297.4762 3869.8394,-9464.1388 3983,-9314.5998 4059.0904,-9214.0482 4026.4237,-7170.8512 4041,-7045.5998 4051.4363,-6955.9231 4077.5286,-6851.5193 4089.6394,-6805.9125"/> +<polygon fill="#000000" stroke="#000000" points="4091.3771,-6806.1879 4090.9756,-6800.9057 4087.9954,-6805.2854 4091.3771,-6806.1879"/> +</g> +<!-- github.com/klauspost/compress/huff0->fmt --> +<g id="edge436" class="edge"> +<title>github.com/klauspost/compress/huff0->fmt</title> +<path fill="none" stroke="#000000" d="M3465.572,-10025.5116C3486.0061,-10019.3975 3505.9288,-10010.2375 3522,-9996.5998 3572.7275,-9953.5535 3536.8448,-9909.2348 3580,-9858.5998 3716.8732,-9698.0036 3868.8202,-9785.0496 3983,-9607.5998 4076.5748,-9462.1728 4093.1086,-8888.3327 4095.6005,-8765.0196"/> +<polygon fill="#000000" stroke="#000000" points="4097.354,-8764.855 4095.7026,-8759.8216 4093.8547,-8764.7863 4097.354,-8764.855"/> +</g> +<!-- github.com/klauspost/compress/huff0->io --> +<g id="edge438" class="edge"> +<title>github.com/klauspost/compress/huff0->io</title> +<path fill="none" stroke="#000000" d="M3418.8456,-10054.6117C3455.6649,-10068.5035 3498.9864,-10091.7302 3522,-10128.5998 3665.7165,-10358.8451 3471.2764,-11099.9102 3580,-11348.5998 3682.3518,-11582.7148 3882.7836,-11523.5628 3983,-11758.5998 4069.2819,-11960.9562 3997.9275,-13521.8745 4041,-13737.5998 4049.916,-13782.2547 4071.278,-13831.1656 4084.602,-13858.9467"/> +<polygon fill="#000000" stroke="#000000" points="4083.0767,-13859.8124 4086.8297,-13863.5511 4086.2273,-13858.288 4083.0767,-13859.8124"/> +</g> +<!-- github.com/klauspost/compress/huff0->math --> +<g id="edge439" class="edge"> +<title>github.com/klauspost/compress/huff0->math</title> +<path fill="none" stroke="#000000" d="M3356.0957,-10018.572C3370.5411,-9854.5284 3479.1728,-8602.9994 3522,-7585.5998 3526.26,-7484.3988 3514.64,-4016.981 3580,-3939.5998 3703.3478,-3793.5655 3971.0451,-3822.4055 4063.6806,-3837.578"/> +<polygon fill="#000000" stroke="#000000" points="4063.5249,-3839.3262 4068.7447,-3838.4226 4064.1007,-3835.8738 4063.5249,-3839.3262"/> +</g> +<!-- github.com/klauspost/compress/huff0->sync --> +<g id="edge442" class="edge"> +<title>github.com/klauspost/compress/huff0->sync</title> +<path fill="none" stroke="#000000" d="M3356.1099,-10018.5726C3370.6811,-9854.5342 3480.2115,-8603.0426 3522,-7585.5998 3526.7688,-7469.4921 3509.4708,-3494.9544 3580,-3402.5998 3693.1226,-3254.4713 3819.1966,-3375.5224 3983,-3286.5998 4019.8915,-3266.5728 4055.2484,-3232.7634 4076.2995,-3210.5621"/> +<polygon fill="#000000" stroke="#000000" points="4077.6576,-3211.6723 4079.8068,-3206.8305 4075.1073,-3209.2753 4077.6576,-3211.6723"/> +</g> +<!-- github.com/klauspost/compress/huff0->math/bits --> +<g id="edge440" class="edge"> +<title>github.com/klauspost/compress/huff0->math/bits</title> +<path fill="none" stroke="#000000" d="M3465.8199,-10043.4291C3487.1078,-10049.7429 3507.3037,-10060.0621 3522,-10076.5998 3669.1381,-10242.1742 3426.991,-10411.435 3580,-10571.5998 3642.6864,-10637.2179 3893.8134,-10620.8351 3983,-10637.5998 4007.6342,-10642.2304 4035.1482,-10648.0599 4056.8587,-10652.814"/> +<polygon fill="#000000" stroke="#000000" points="4056.7048,-10654.5718 4061.9639,-10653.9355 4057.4558,-10651.1534 4056.7048,-10654.5718"/> +</g> +<!-- github.com/klauspost/compress/huff0->runtime --> +<g id="edge441" class="edge"> +<title>github.com/klauspost/compress/huff0->runtime</title> +<path fill="none" stroke="#000000" d="M3420.2125,-10054.6123C3457.033,-10068.4266 3499.8812,-10091.5724 3522,-10128.5998 3619.9969,-10292.6487 3474.3493,-16848.3724 3580,-17007.5998 3687.8638,-17170.1625 3861.2043,-17029.1956 3983,-17181.5998 4057.062,-17274.2744 4087.7942,-17662.2549 4094.5256,-17761.434"/> +<polygon fill="#000000" stroke="#000000" points="4092.7831,-17761.6054 4094.864,-17766.477 4096.2752,-17761.371 4092.7831,-17761.6054"/> +</g> +<!-- github.com/klauspost/compress/huff0->github.com/klauspost/compress/fse --> +<g id="edge437" class="edge"> +<title>github.com/klauspost/compress/huff0->github.com/klauspost/compress/fse</title> +<path fill="none" stroke="#000000" d="M3465.6021,-10043.6243C3486.9037,-10049.9259 3507.1601,-10060.1908 3522,-10076.5998 3651.3498,-10219.6263 3445.2463,-10368.6528 3580,-10506.5998 3603.7295,-10530.8916 3637.9457,-10541.477 3671.466,-10545.215"/> +<polygon fill="#000000" stroke="#000000" points="3671.6877,-10546.9962 3676.8382,-10545.7572 3672.0392,-10543.5139 3671.6877,-10546.9962"/> +</g> +<!-- github.com/klauspost/compress/snappy->encoding/binary --> +<g id="edge443" class="edge"> +<title>github.com/klauspost/compress/snappy->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3421.8431,-12173.6645C3538.8148,-12201.8764 3785.8915,-12247.3969 3983,-12188.5998 4017.2714,-12178.3767 4050.813,-12154.0716 4072.3316,-12136.0909"/> +<polygon fill="#000000" stroke="#000000" points="4073.5204,-12137.3775 4076.2071,-12132.812 4071.2597,-12134.7055 4073.5204,-12137.3775"/> +</g> +<!-- github.com/klauspost/compress/snappy->errors --> +<g id="edge444" class="edge"> +<title>github.com/klauspost/compress/snappy->errors</title> +<path fill="none" stroke="#000000" d="M3369.1922,-12137.5988C3403.807,-12093.9251 3489.9705,-11977.5299 3522,-11863.5998 3553.9325,-11750.0146 3506.0447,-9832.5339 3580,-9740.5998 3695.6718,-9596.8078 3867.6856,-9784.6786 3983,-9640.5998 4073.1072,-9528.016 4024.5606,-7188.8624 4041,-7045.5998 4051.2924,-6955.9064 4077.4584,-6851.5112 4089.6137,-6805.9095"/> +<polygon fill="#000000" stroke="#000000" points="4091.3513,-6806.1859 4090.9549,-6800.9033 4087.9705,-6805.2801 4091.3513,-6806.1859"/> +</g> +<!-- github.com/klauspost/compress/snappy->io --> +<g id="edge446" class="edge"> +<title>github.com/klauspost/compress/snappy->io</title> +<path fill="none" stroke="#000000" d="M3397.8502,-12173.6633C3517.4691,-12226.1706 3851.9669,-12391.5996 3983,-12649.5998 4092.6385,-12865.4748 3990.752,-13500.7502 4041,-13737.5998 4050.4503,-13782.1447 4071.6363,-13831.0918 4084.7822,-13858.9096"/> +<polygon fill="#000000" stroke="#000000" points="4083.2485,-13859.7593 4086.9792,-13863.5203 4086.4082,-13858.2538 4083.2485,-13859.7593"/> +</g> +<!-- github.com/klauspost/compress/snappy->hash/crc32 --> +<g id="edge445" class="edge"> +<title>github.com/klauspost/compress/snappy->hash/crc32</title> +<path fill="none" stroke="#000000" d="M3470.0523,-12155.5998C3558.6989,-12155.5998 3676.1077,-12155.5998 3738.3938,-12155.5998"/> +<polygon fill="#000000" stroke="#000000" points="3738.5143,-12157.3499 3743.5143,-12155.5998 3738.5142,-12153.8499 3738.5143,-12157.3499"/> +</g> +<!-- github.com/klauspost/compress/zstd/internal/xxhash->encoding/binary --> +<g id="edge468" class="edge"> +<title>github.com/klauspost/compress/zstd/internal/xxhash->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3858.5737,-11223.6514C3902.3101,-11238.0618 3953.9612,-11262.9576 3983,-11304.5998 4054.1833,-11406.6782 4021.3075,-11738.7207 4041,-11861.5998 4054.6977,-11947.0721 4078.9111,-12047.1207 4090.0677,-12091.4472"/> +<polygon fill="#000000" stroke="#000000" points="4088.3771,-12091.9005 4091.2981,-12096.3199 4091.7706,-12091.0436 4088.3771,-12091.9005"/> +</g> +<!-- github.com/klauspost/compress/zstd/internal/xxhash->errors --> +<g id="edge469" class="edge"> +<title>github.com/klauspost/compress/zstd/internal/xxhash->errors</title> +<path fill="none" stroke="#000000" d="M3825.1706,-11187.5334C3873.7431,-11164.7843 3949.7433,-11120.6395 3983,-11055.5998 4084.4337,-10857.2276 4016.4729,-7267.0468 4041,-7045.5998 4050.9387,-6955.8666 4077.2858,-6851.4917 4089.5505,-6805.9024"/> +<polygon fill="#000000" stroke="#000000" points="4091.288,-6806.1811 4090.9041,-6800.8976 4087.9094,-6805.2673 4091.288,-6806.1811"/> +</g> +<!-- github.com/klauspost/compress/zstd/internal/xxhash->math/bits --> +<g id="edge470" class="edge"> +<title>github.com/klauspost/compress/zstd/internal/xxhash->math/bits</title> +<path fill="none" stroke="#000000" d="M3851.7804,-11187.5469C3895.6336,-11172.7645 3949.8565,-11147.4684 3983,-11106.5998 4037.4711,-11039.4326 4080.785,-10766.4272 4092.7218,-10684.7757"/> +<polygon fill="#000000" stroke="#000000" points="4094.4753,-10684.8781 4093.4625,-10679.6784 4091.0116,-10684.3747 4094.4753,-10684.8781"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil --> +<g id="node121" class="node"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil</title> +<g id="a_node121"><a xlink:href="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" xlink:title="github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3510,-10178.5998C3510,-10178.5998 3199,-10178.5998 3199,-10178.5998 3193,-10178.5998 3187,-10172.5998 3187,-10166.5998 3187,-10166.5998 3187,-10154.5998 3187,-10154.5998 3187,-10148.5998 3193,-10142.5998 3199,-10142.5998 3199,-10142.5998 3510,-10142.5998 3510,-10142.5998 3516,-10142.5998 3522,-10148.5998 3522,-10154.5998 3522,-10154.5998 3522,-10166.5998 3522,-10166.5998 3522,-10172.5998 3516,-10178.5998 3510,-10178.5998"/> +<text text-anchor="middle" x="3354.5" y="-10156.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/matttproud/golang_protobuf_extensions/pbutil</text> +</a> +</g> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->encoding/binary --> +<g id="edge481" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3355.9471,-10178.7092C3364.8514,-10284.2615 3417.2967,-10829.5222 3580,-11238.5998 3706.4596,-11556.5517 3815.1531,-11596.4172 3983,-11894.5998 4022.7707,-11965.253 4065.3492,-12051.3694 4084.9565,-12091.6993"/> +<polygon fill="#000000" stroke="#000000" points="4083.474,-12092.6529 4087.232,-12096.3865 4086.6226,-12091.1243 4083.474,-12092.6529"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->errors --> +<g id="edge482" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->errors</title> +<path fill="none" stroke="#000000" d="M3418.1426,-10142.5247C3454.7051,-10128.6893 3498.0036,-10105.7066 3522,-10069.5998 3593.4518,-9962.0882 3494.686,-9588.4792 3580,-9491.5998 3701.4838,-9353.6472 3866.7997,-9541.0313 3983,-9398.5998 4065.6608,-9297.2791 4025.9436,-7175.492 4041,-7045.5998 4051.3954,-6955.9183 4077.5086,-6851.517 4089.6321,-6805.9117"/> +<polygon fill="#000000" stroke="#000000" points="4091.3698,-6806.1873 4090.9698,-6800.905 4087.9884,-6805.2839 4091.3698,-6806.1873"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->io --> +<g id="edge484" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->io</title> +<path fill="none" stroke="#000000" d="M3358.1658,-10178.8234C3377.7014,-10276.5045 3470.5975,-10748.1759 3522,-11137.5998 3540.2243,-11275.6666 3500.4,-11647.3264 3580,-11761.5998 3693.5099,-11924.5542 3874.4428,-11788.3051 3983,-11954.5998 4091.3519,-12120.58 4001.8768,-13543.2833 4041,-13737.5998 4049.9878,-13782.2403 4071.3262,-13831.1559 4084.6262,-13858.9418"/> +<polygon fill="#000000" stroke="#000000" points="4083.0998,-13859.8054 4086.8498,-13863.5471 4086.2516,-13858.2835 4083.0998,-13859.8054"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->github.com/golang/protobuf/proto --> +<g id="edge483" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3420.8415,-10142.5344C3457.3929,-10128.8577 3499.7509,-10106.0355 3522,-10069.5998 3594.113,-9951.5058 3564.3506,-7717.0829 3580,-7579.5998 3623.9655,-7193.3551 3745.7418,-6733.3332 3775.0803,-6625.8459"/> +<polygon fill="#000000" stroke="#000000" points="3776.855,-6625.9902 3776.4863,-6620.7057 3773.479,-6625.0668 3776.855,-6625.9902"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal->sort --> +<g id="edge535" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/internal->sort</title> +<path fill="none" stroke="#000000" d="M2975.4948,-6197.5788C3012.3749,-6160.3143 3094.481,-6070.3909 3129,-5976.5998 3183.6766,-5828.0387 3092.8194,-5393.8396 3187,-5266.5998 3282.3052,-5137.8406 3428.0546,-5263.3545 3522,-5133.5998 3647.077,-4960.847 3424.6991,-4321.7827 3580,-4175.5998 3710.4214,-4052.8357 3841.9895,-4065.1606 3983,-4175.5998 4067.6944,-4241.9323 3976.5035,-4324.4991 4041,-4410.5998 4046.9794,-4418.5822 4055.5818,-4424.9142 4064.2134,-4429.7672"/> +<polygon fill="#000000" stroke="#000000" points="4063.7592,-4431.5079 4068.9952,-4432.3122 4065.4037,-4428.4182 4063.7592,-4431.5079"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal->github.com/prometheus/client_model/go --> +<g id="edge534" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/internal->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M2999.0609,-6233.692C3035.2462,-6250.2574 3087.9477,-6276.8286 3129,-6307.5998 3215.912,-6372.7457 3300.752,-6471.4392 3336.6318,-6515.271"/> +<polygon fill="#000000" stroke="#000000" points="3335.4331,-6516.5701 3339.9496,-6519.3385 3338.1452,-6514.3578 3335.4331,-6516.5701"/> +</g> +<!-- github.com/prometheus/client_model/go->fmt --> +<g id="edge552" class="edge"> +<title>github.com/prometheus/client_model/go->fmt</title> +<path fill="none" stroke="#000000" d="M3418.7554,-6555.6682C3455.5417,-6569.5807 3498.8602,-6592.8092 3522,-6629.5998 3585.1539,-6730.01 3499.7067,-7608.2866 3580,-7695.5998 3702.4047,-7828.7063 3856.7034,-7622.1803 3983,-7751.5998 4018.0404,-7787.5066 4082.3845,-8571.5202 4094.1331,-8718.1059"/> +<polygon fill="#000000" stroke="#000000" points="4092.4167,-8718.597 4094.56,-8723.4415 4095.9056,-8718.3178 4092.4167,-8718.597"/> +</g> +<!-- github.com/prometheus/client_model/go->math --> +<g id="edge554" class="edge"> +<title>github.com/prometheus/client_model/go->math</title> +<path fill="none" stroke="#000000" d="M3358.1502,-6519.3004C3378.8191,-6414.7869 3481.6721,-5882.6003 3522,-5442.5998 3531.4867,-5339.0948 3505.3736,-3644.9475 3580,-3572.5998 3708.5989,-3447.9277 3833.9575,-3473.2664 3983,-3572.5998 4068.8819,-3629.8382 4089.5999,-3766.0516 4094.5091,-3820.515"/> +<polygon fill="#000000" stroke="#000000" points="4092.7678,-3820.6919 4094.9365,-3825.525 4096.2551,-3820.3943 4092.7678,-3820.6919"/> +</g> +<!-- github.com/prometheus/client_model/go->github.com/golang/protobuf/proto --> +<g id="edge553" class="edge"> +<title>github.com/prometheus/client_model/go->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3472.8763,-6555.6196C3536.2533,-6565.2672 3613.6875,-6577.0546 3675.1376,-6586.4088"/> +<polygon fill="#000000" stroke="#000000" points="3675.0751,-6588.1694 3680.2816,-6587.1919 3675.6019,-6584.7093 3675.0751,-6588.1694"/> +</g> +<!-- github.com/prometheus/common/expfmt->bufio --> +<g id="edge555" class="edge"> +<title>github.com/prometheus/common/expfmt->bufio</title> +<path fill="none" stroke="#000000" d="M2966.9185,-6358.0017C2998.4099,-6417.5934 3095.9942,-6611.5196 3129,-6785.5998 3151.4682,-6904.1024 3108.6746,-15377.8784 3187,-15469.5998 3244.4839,-15536.9152 3491.7773,-15527.3533 3580,-15534.5998 3669.255,-15541.9312 3918.5053,-15596.7338 3983,-15534.5998 4107.5149,-15414.6426 3953.0237,-14128.4416 4041,-13979.5998 4046.3562,-13970.5379 4055.2563,-13963.6687 4064.3526,-13958.6085"/> +<polygon fill="#000000" stroke="#000000" points="4065.2026,-13960.1389 4068.8192,-13956.2681 4063.5781,-13957.0387 4065.2026,-13960.1389"/> +</g> +<!-- github.com/prometheus/common/expfmt->bytes --> +<g id="edge556" class="edge"> +<title>github.com/prometheus/common/expfmt->bytes</title> +<path fill="none" stroke="#000000" d="M2966.8844,-6358.0082C2998.2729,-6417.6195 3095.5815,-6611.5984 3129,-6785.5998 3155.6064,-6924.1327 3102.5347,-11750.6181 3187,-11863.5998 3280.2689,-11988.3573 3373.8134,-11918.5973 3522,-11966.5998 3727.911,-12033.3012 3850.8157,-11952.2064 3983,-12123.5998 4082.4836,-12252.5927 4094.6078,-13495.5856 4095.8672,-13681.4072"/> +<polygon fill="#000000" stroke="#000000" points="4094.1183,-13681.6061 4095.9013,-13686.5944 4097.6183,-13681.5829 4094.1183,-13681.6061"/> +</g> +<!-- github.com/prometheus/common/expfmt->fmt --> +<g id="edge557" class="edge"> +<title>github.com/prometheus/common/expfmt->fmt</title> +<path fill="none" stroke="#000000" d="M3018.0213,-6357.759C3056.9037,-6372.7739 3104.436,-6398.2926 3129,-6438.5998 3274.5192,-6677.383 3024.6415,-7467.9313 3187,-7695.5998 3280.0113,-7826.0257 3402.6129,-7721.7881 3522,-7828.5998 3562.8972,-7865.1892 3538.0386,-7902.236 3580,-7937.5998 3724.2251,-8059.1482 3860.1367,-7927.4931 3983,-8070.5998 4069.4587,-8171.3037 4091.1997,-8612.2003 4095.223,-8718.4859"/> +<polygon fill="#000000" stroke="#000000" points="4093.4777,-8718.6478 4095.4119,-8723.5795 4096.9753,-8718.518 4093.4777,-8718.6478"/> +</g> +<!-- github.com/prometheus/common/expfmt->io --> +<g id="edge563" class="edge"> +<title>github.com/prometheus/common/expfmt->io</title> +<path fill="none" stroke="#000000" d="M2966.9073,-6358.0038C2998.3649,-6417.6019 3095.8587,-6611.5454 3129,-6785.5998 3165.5898,-6977.7651 3095.6369,-13654.6285 3187,-13827.5998 3286.3292,-14015.6528 3377.0502,-14037.0264 3580,-14100.5998 3750.9216,-14154.1405 3838.7243,-14206.738 3983,-14100.5998 4052.7511,-14049.2865 3987.4771,-13982.6701 4041,-13914.5998 4047.1559,-13906.7708 4055.7235,-13900.3843 4064.2664,-13895.388"/> +<polygon fill="#000000" stroke="#000000" points="4065.4776,-13896.7166 4068.9944,-13892.7549 4063.7747,-13893.6588 4065.4776,-13896.7166"/> +</g> +<!-- github.com/prometheus/common/expfmt->io/ioutil --> +<g id="edge564" class="edge"> +<title>github.com/prometheus/common/expfmt->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2966.9188,-6358.0016C2998.4111,-6417.5932 3095.9977,-6611.5189 3129,-6785.5998 3151.6029,-6904.8261 3108.0098,-15430.4784 3187,-15522.5998 3236.0836,-15579.843 3450.8074,-15551.7485 3522,-15576.5998 3743.881,-15654.052 3841.0263,-15648.3206 3983,-15835.5998 4037.6358,-15907.6706 4081.072,-16193.4859 4092.8406,-16277.3517"/> +<polygon fill="#000000" stroke="#000000" points="4091.1457,-16277.8691 4093.5697,-16282.5793 4094.6122,-16277.3855 4091.1457,-16277.8691"/> +</g> +<!-- github.com/prometheus/common/expfmt->math --> +<g id="edge565" class="edge"> +<title>github.com/prometheus/common/expfmt->math</title> +<path fill="none" stroke="#000000" d="M3026.4328,-6321.5566C3063.6624,-6307.9931 3106.4153,-6285.2772 3129,-6248.5998 3214.7742,-6109.303 3099.701,-3442.9461 3187,-3304.5998 3284.2454,-3150.4911 3844.7152,-2988.9259 3983,-3107.5998 4038.8242,-3155.5073 4084.7105,-3700.0642 4094.1994,-3820.2298"/> +<polygon fill="#000000" stroke="#000000" points="4092.4618,-3820.4572 4094.5984,-3825.3046 4095.9511,-3820.1828 4092.4618,-3820.4572"/> +</g> +<!-- github.com/prometheus/common/expfmt->strconv --> +<g id="edge568" class="edge"> +<title>github.com/prometheus/common/expfmt->strconv</title> +<path fill="none" stroke="#000000" d="M3078.0649,-6330.9102C3097.3276,-6324.4499 3115.3646,-6314.5498 3129,-6299.5998 3256.5773,-6159.7227 3055.0898,-6013.3985 3187,-5877.5998 3292.1797,-5769.3197 3417.9935,-5931.0074 3522,-5821.5998 3599.3721,-5740.2098 3496.1604,-5394.3105 3580,-5319.5998 3719.3993,-5195.3791 3972.9762,-5294.1747 4063.1174,-5335.5637"/> +<polygon fill="#000000" stroke="#000000" points="4062.5246,-5337.2176 4067.7974,-5337.7286 4063.9941,-5334.041 4062.5246,-5337.2176"/> +</g> +<!-- github.com/prometheus/common/expfmt->strings --> +<g id="edge569" class="edge"> +<title>github.com/prometheus/common/expfmt->strings</title> +<path fill="none" stroke="#000000" d="M3078.1036,-6350.0605C3322.1915,-6373.7187 3860.4579,-6439.7457 3983,-6570.5998 3991.5729,-6579.7542 4070.6175,-6956.0488 4091.1468,-7054.321"/> +<polygon fill="#000000" stroke="#000000" points="4089.4567,-7054.7887 4092.1919,-7059.3253 4092.8828,-7054.0732 4089.4567,-7054.7887"/> +</g> +<!-- github.com/prometheus/common/expfmt->sync --> +<g id="edge570" class="edge"> +<title>github.com/prometheus/common/expfmt->sync</title> +<path fill="none" stroke="#000000" d="M3026.4495,-6321.5669C3063.6846,-6308.0067 3106.4374,-6285.2908 3129,-6248.5998 3220.2766,-6100.1668 3115.2259,-3271.3834 3187,-3112.5998 3284.4437,-2897.0282 3359.4465,-2839.1718 3580,-2753.5998 3746.9832,-2688.8124 3841.3195,-2644.0214 3983,-2753.5998 4050.0386,-2805.4488 4084.6706,-3082.5251 4093.6226,-3165.2588"/> +<polygon fill="#000000" stroke="#000000" points="4091.903,-3165.6374 4094.1752,-3170.4228 4095.3832,-3165.265 4091.903,-3165.6374"/> +</g> +<!-- github.com/prometheus/common/expfmt->mime --> +<g id="edge566" class="edge"> +<title>github.com/prometheus/common/expfmt->mime</title> +<path fill="none" stroke="#000000" d="M3025.0207,-6321.5359C3062.2715,-6307.896 3105.5163,-6285.1105 3129,-6248.5998 3247.2555,-6064.7447 3110.3646,-5471.3291 3187,-5266.5998 3217.8268,-5184.2469 3291.4094,-5107.7304 3329.9717,-5071.5247"/> +<polygon fill="#000000" stroke="#000000" points="3331.5793,-5072.4186 3334.0437,-5067.7293 3329.1929,-5069.8583 3331.5793,-5072.4186"/> +</g> +<!-- github.com/prometheus/common/expfmt->net/http --> +<g id="edge567" class="edge"> +<title>github.com/prometheus/common/expfmt->net/http</title> +<path fill="none" stroke="#000000" d="M3026.9451,-6321.442C3064.0953,-6307.8748 3106.6011,-6285.193 3129,-6248.5998 3187.4777,-6153.0647 3165.1061,-2326.4509 3187,-2216.5998 3265.6417,-1822.02 3418.2433,-1764.3315 3522,-1375.5998 3568.779,-1200.3388 3487.9115,-1127.8829 3580,-971.5998 3699.2395,-769.2387 3967.6415,-636.7118 4062.2298,-594.8497"/> +<polygon fill="#000000" stroke="#000000" points="4062.9988,-596.4233 4066.8701,-592.8072 4061.5887,-593.2199 4062.9988,-596.4233"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/golang/protobuf/proto --> +<g id="edge558" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3075.06,-6357.6762C3093.7857,-6363.1591 3112.4223,-6370.3003 3129,-6379.5998 3161.0697,-6397.5897 3154.6545,-6421.1108 3187,-6438.5998 3320.5638,-6510.817 3375.9736,-6463.9956 3522,-6505.5998 3597.9536,-6527.2397 3683.234,-6561.0266 3734.6278,-6582.4749"/> +<polygon fill="#000000" stroke="#000000" points="3734.2061,-6584.1955 3739.4941,-6584.5112 3735.5572,-6580.9667 3734.2061,-6584.1955"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/matttproud/golang_protobuf_extensions/pbutil --> +<g id="edge559" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/matttproud/golang_protobuf_extensions/pbutil</title> +<path fill="none" stroke="#000000" d="M2966.8415,-6358.0165C2998.1005,-6417.6529 3095.0624,-6611.6989 3129,-6785.5998 3146.7469,-6876.5372 3128.9652,-10048.3743 3187,-10120.5998 3193.0833,-10128.1706 3200.3198,-10134.4641 3208.3188,-10139.6826"/> +<polygon fill="#000000" stroke="#000000" points="3207.6647,-10141.3354 3212.8387,-10142.4725 3209.5031,-10138.357 3207.6647,-10141.3354"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/client_model/go --> +<g id="edge560" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M3078.4218,-6353.4762C3096.6386,-6359.4724 3114.2357,-6367.8733 3129,-6379.5998 3174.7599,-6415.9446 3141.3478,-6461.1198 3187,-6497.5998 3199.7001,-6507.7483 3214.5618,-6515.3902 3230.094,-6521.1326"/> +<polygon fill="#000000" stroke="#000000" points="3229.6368,-6522.8273 3234.9342,-6522.8544 3230.8099,-6519.5297 3229.6368,-6522.8273"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/common/model --> +<g id="edge562" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/common/model</title> +<path fill="none" stroke="#000000" d="M3024.919,-6321.4702C3062.1353,-6307.808 3105.3796,-6285.0222 3129,-6248.5998 3234.5488,-6085.8444 3071.3452,-5533.3363 3187,-5377.5998 3281.8398,-5249.8919 3418.8063,-5372.6575 3522,-5251.5998 3605.856,-5153.2275 3488.378,-5057.7828 3580,-4966.5998 3600.7905,-4945.9089 3629.019,-4934.384 3657.7607,-4928.2712"/> +<polygon fill="#000000" stroke="#000000" points="3658.1977,-4929.9684 3662.7527,-4927.2639 3657.5054,-4926.5375 3658.1977,-4929.9684"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg --> +<g id="node129" class="node"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title> +<g id="a_node129"><a xlink:href="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" xlink:title="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3971,-5369.5998C3971,-5369.5998 3592,-5369.5998 3592,-5369.5998 3586,-5369.5998 3580,-5363.5998 3580,-5357.5998 3580,-5357.5998 3580,-5345.5998 3580,-5345.5998 3580,-5339.5998 3586,-5333.5998 3592,-5333.5998 3592,-5333.5998 3971,-5333.5998 3971,-5333.5998 3977,-5333.5998 3983,-5339.5998 3983,-5345.5998 3983,-5345.5998 3983,-5357.5998 3983,-5357.5998 3983,-5363.5998 3977,-5369.5998 3971,-5369.5998"/> +<text text-anchor="middle" x="3781.5" y="-5347.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</text> +</a> +</g> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg --> +<g id="edge561" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title> +<path fill="none" stroke="#000000" d="M3076.1548,-6321.5888C3094.5516,-6316.1001 3112.7981,-6308.94 3129,-6299.5998 3350.1942,-6172.084 3406.6902,-6108.3953 3522,-5880.5998 3585.6993,-5754.7613 3516.6162,-5694.5976 3580,-5568.5998 3622.6374,-5483.8428 3708.5163,-5408.5162 3753.1346,-5373.0698"/> +<polygon fill="#000000" stroke="#000000" points="3754.4807,-5374.2368 3757.3209,-5369.765 3752.312,-5371.4896 3754.4807,-5374.2368"/> +</g> +<!-- github.com/prometheus/common/model->encoding/json --> +<g id="edge574" class="edge"> +<title>github.com/prometheus/common/model->encoding/json</title> +<path fill="none" stroke="#000000" d="M3872.3035,-4944.6681C3913.4491,-4957.8648 3958.3218,-4980.4038 3983,-5018.5998 4074.857,-5160.773 3956.2634,-17059.0712 4041,-17205.5998 4043.2657,-17209.5178 4046.2149,-17213.0012 4049.5706,-17216.089"/> +<polygon fill="#000000" stroke="#000000" points="4048.7243,-17217.663 4053.6803,-17219.5342 4050.9729,-17214.9809 4048.7243,-17217.663"/> +</g> +<!-- github.com/prometheus/common/model->fmt --> +<g id="edge575" class="edge"> +<title>github.com/prometheus/common/model->fmt</title> +<path fill="none" stroke="#000000" d="M3871.5402,-4944.6585C3912.7688,-4957.8701 3957.9373,-4980.4247 3983,-5018.5998 4028.5754,-5088.0195 4037.0903,-7924.6485 4041,-8007.5998 4054.2131,-8287.9426 4085.2354,-8627.4988 4093.7846,-8718.3337"/> +<polygon fill="#000000" stroke="#000000" points="4092.0566,-8718.65 4094.2684,-8723.4636 4095.5411,-8718.3213 4092.0566,-8718.65"/> +</g> +<!-- github.com/prometheus/common/model->math --> +<g id="edge576" class="edge"> +<title>github.com/prometheus/common/model->math</title> +<path fill="none" stroke="#000000" d="M3808.2227,-4908.4191C3853.3078,-4876.0544 3943.4294,-4803.6976 3983,-4717.5998 4097.0979,-4469.3456 3999.9805,-4375.7218 4041,-4105.5998 4054.505,-4016.6663 4079.026,-3912.4535 4090.1874,-3866.8957"/> +<polygon fill="#000000" stroke="#000000" points="4091.9227,-3867.1672 4091.417,-3861.894 4088.5239,-3866.3316 4091.9227,-3867.1672"/> +</g> +<!-- github.com/prometheus/common/model->sort --> +<g id="edge578" class="edge"> +<title>github.com/prometheus/common/model->sort</title> +<path fill="none" stroke="#000000" d="M3815.3005,-4908.3953C3859.8014,-4882.797 3937.6853,-4832.1088 3983,-4768.5998 4053.6111,-4669.6377 4083.2848,-4522.6931 4092.5505,-4466.0443"/> +<polygon fill="#000000" stroke="#000000" points="4094.3199,-4466.0628 4093.3842,-4460.8487 4090.8641,-4465.5082 4094.3199,-4466.0628"/> +</g> +<!-- github.com/prometheus/common/model->strconv --> +<g id="edge579" class="edge"> +<title>github.com/prometheus/common/model->strconv</title> +<path fill="none" stroke="#000000" d="M3856.0207,-4944.7244C3898.4395,-4958.5405 3949.6108,-4981.6497 3983,-5018.5998 4067.0335,-5111.5954 4088.9589,-5269.6017 4094.3472,-5328.5355"/> +<polygon fill="#000000" stroke="#000000" points="4092.6109,-5328.7684 4094.7912,-5333.5963 4096.0975,-5328.4625 4092.6109,-5328.7684"/> +</g> +<!-- github.com/prometheus/common/model->strings --> +<g id="edge580" class="edge"> +<title>github.com/prometheus/common/model->strings</title> +<path fill="none" stroke="#000000" d="M3870.809,-4944.6314C3912.1253,-4957.8542 3957.5857,-4980.4267 3983,-5018.5998 4038.3543,-5101.7441 4029.083,-6716.4279 4041,-6815.5998 4051.732,-6904.9104 4077.6729,-7008.9301 4089.6922,-7054.3713"/> +<polygon fill="#000000" stroke="#000000" points="4088.0424,-7054.9772 4091.0182,-7059.3599 4091.425,-7054.0781 4088.0424,-7054.9772"/> +</g> +<!-- github.com/prometheus/common/model->time --> +<g id="edge581" class="edge"> +<title>github.com/prometheus/common/model->time</title> +<path fill="none" stroke="#000000" d="M3871.9698,-4944.6316C3913.1689,-4957.828 3958.1856,-4980.3775 3983,-5018.5998 4078.8251,-5166.2019 3952.5678,-11201.4533 4041,-11353.5998 4046.2955,-11362.7106 4055.2811,-11369.4748 4064.4715,-11374.376"/> +<polygon fill="#000000" stroke="#000000" points="4063.7298,-11375.9617 4068.9843,-11376.6346 4065.2963,-11372.8318 4063.7298,-11375.9617"/> +</g> +<!-- github.com/prometheus/common/model->unicode/utf8 --> +<g id="edge582" class="edge"> +<title>github.com/prometheus/common/model->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3806.6958,-4908.5364C3850.2811,-4875.8156 3939.2708,-4802.2296 3983,-4717.5998 4046.743,-4594.2369 3961.6177,-4524.5298 4041,-4410.5998 4044.0706,-4406.1929 4047.9366,-4402.2752 4052.1893,-4398.8187"/> +<polygon fill="#000000" stroke="#000000" points="4053.2723,-4400.1936 4056.1918,-4395.7732 4051.1529,-4397.4082 4053.2723,-4400.1936"/> +</g> +<!-- github.com/prometheus/common/model->regexp --> +<g id="edge577" class="edge"> +<title>github.com/prometheus/common/model->regexp</title> +<path fill="none" stroke="#000000" d="M3809.1928,-4908.4281C3855.3051,-4876.3373 3946.3717,-4804.7167 3983,-4717.5998 4069.4675,-4511.945 4023.0288,-2932.9679 4041,-2710.5998 4053.0718,-2561.2275 4081.6261,-2383.1353 4092.0787,-2320.6372"/> +<polygon fill="#000000" stroke="#000000" points="4093.815,-2320.8642 4092.9166,-2315.6436 4090.3632,-2320.285 4093.815,-2320.8642"/> +</g> +<!-- github.com/prometheus/procfs->bufio --> +<g id="edge583" class="edge"> +<title>github.com/prometheus/procfs->bufio</title> +<path fill="none" stroke="#000000" d="M3392.2487,-13812.6907C3436.2792,-13832.9926 3511.7576,-13865.3393 3580,-13882.5998 3759.1885,-13927.9219 3981.8322,-13941.6866 4063.7887,-13945.3812"/> +<polygon fill="#000000" stroke="#000000" points="4063.9093,-13947.1381 4068.9816,-13945.6102 4064.0636,-13943.6415 4063.9093,-13947.1381"/> +</g> +<!-- github.com/prometheus/procfs->bytes --> +<g id="edge584" class="edge"> +<title>github.com/prometheus/procfs->bytes</title> +<path fill="none" stroke="#000000" d="M3446.5592,-13810.0752C3571.5953,-13827.3735 3800.4458,-13845.7158 3983,-13785.5998 4018.828,-13773.8015 4053.0225,-13746.2237 4074.2268,-13726.5279"/> +<polygon fill="#000000" stroke="#000000" points="4075.5899,-13727.6483 4078.0324,-13722.9475 4073.1916,-13725.0991 4075.5899,-13727.6483"/> +</g> +<!-- github.com/prometheus/procfs->encoding/hex --> +<g id="edge585" class="edge"> +<title>github.com/prometheus/procfs->encoding/hex</title> +<path fill="none" stroke="#000000" d="M3364.7539,-13776.3918C3395.922,-13719.8981 3489.3948,-13541.7075 3522,-13380.5998 3564.8616,-13168.8139 3497.8833,-9691.4678 3580,-9491.5998 3614.9487,-9406.5365 3702.0551,-9336.2784 3749.4891,-9302.7705"/> +<polygon fill="#000000" stroke="#000000" points="3750.5833,-9304.1407 3753.673,-9299.8377 3748.5742,-9301.2747 3750.5833,-9304.1407"/> +</g> +<!-- github.com/prometheus/procfs->errors --> +<g id="edge586" class="edge"> +<title>github.com/prometheus/procfs->errors</title> +<path fill="none" stroke="#000000" d="M3364.7304,-13776.387C3395.8309,-13719.8795 3489.1298,-13541.6536 3522,-13380.5998 3557.4009,-13207.1462 3491.1274,-10347.7045 3580,-10194.5998 3684.6551,-10014.306 3878.1887,-10134.8029 3983,-9954.5998 4064.2695,-9814.8722 4022.7659,-7206.2113 4041,-7045.5998 4051.1842,-6955.8941 4077.4056,-6851.5052 4089.5944,-6805.9073"/> +<polygon fill="#000000" stroke="#000000" points="4091.3319,-6806.1844 4090.9394,-6800.9016 4087.9518,-6805.2762 4091.3319,-6806.1844"/> +</g> +<!-- github.com/prometheus/procfs->fmt --> +<g id="edge587" class="edge"> +<title>github.com/prometheus/procfs->fmt</title> +<path fill="none" stroke="#000000" d="M3364.0009,-13776.2185C3393.0015,-13719.2259 3480.8989,-13539.7522 3522,-13380.5998 3575.4002,-13173.8224 3486.2547,-13095.486 3580,-12903.5998 3690.914,-12676.5711 3882.5228,-12734.4366 3983,-12502.5998 3985.4778,-12496.8827 4086.0396,-9080.1968 4095.3161,-8764.854"/> +<polygon fill="#000000" stroke="#000000" points="4097.0727,-8764.652 4095.4705,-8759.6027 4093.5742,-8764.5491 4097.0727,-8764.652"/> +</g> +<!-- github.com/prometheus/procfs->io --> +<g id="edge590" class="edge"> +<title>github.com/prometheus/procfs->io</title> +<path fill="none" stroke="#000000" d="M3423.5987,-13812.6799C3467.9796,-13823.6052 3527.0273,-13836.8704 3580,-13844.5998 3761.7524,-13871.12 3982.4177,-13878.8809 4063.8315,-13880.9301"/> +<polygon fill="#000000" stroke="#000000" points="4063.9492,-13882.6834 4068.9908,-13881.0569 4064.0353,-13879.1845 4063.9492,-13882.6834"/> +</g> +<!-- github.com/prometheus/procfs->io/ioutil --> +<g id="edge591" class="edge"> +<title>github.com/prometheus/procfs->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3366.6377,-13812.8476C3399.1587,-13861.3283 3491.7824,-13996.7342 3580,-14100.5998 3746.0236,-14296.0726 3878.8585,-14279.2326 3983,-14513.5998 4058.0362,-14682.4663 4091.1669,-16079.2441 4095.5042,-16277.2823"/> +<polygon fill="#000000" stroke="#000000" points="4093.7599,-16277.5677 4095.6185,-16282.5284 4097.2591,-16277.4914 4093.7599,-16277.5677"/> +</g> +<!-- github.com/prometheus/procfs->sort --> +<g id="edge596" class="edge"> +<title>github.com/prometheus/procfs->sort</title> +<path fill="none" stroke="#000000" d="M3364.8116,-13776.4033C3396.1457,-13719.9429 3490.0457,-13541.8379 3522,-13380.5998 3544.9127,-13264.9845 3515.2211,-4993.066 3580,-4894.5998 3687.5172,-4731.17 3846.3396,-4857.5754 3983,-4717.5998 4054.9883,-4643.8652 4083.4195,-4517.8867 4092.4485,-4465.9936"/> +<polygon fill="#000000" stroke="#000000" points="4094.1992,-4466.1368 4093.3111,-4460.9143 4090.7486,-4465.5507 4094.1992,-4466.1368"/> +</g> +<!-- github.com/prometheus/procfs->strconv --> +<g id="edge597" class="edge"> +<title>github.com/prometheus/procfs->strconv</title> +<path fill="none" stroke="#000000" d="M3364.8,-13776.401C3396.1009,-13719.934 3489.9154,-13541.812 3522,-13380.5998 3559.2073,-13193.6485 3486.7607,-6684.8575 3580,-6518.5998 3683.2141,-6334.5556 3869.4428,-6445.4486 3983,-6267.5998 4081.6689,-6113.0685 4094.2312,-5502.8226 4095.786,-5375.112"/> +<polygon fill="#000000" stroke="#000000" points="4097.5401,-5374.7593 4095.8483,-5369.7393 4094.0404,-5374.7187 4097.5401,-5374.7593"/> +</g> +<!-- github.com/prometheus/procfs->strings --> +<g id="edge598" class="edge"> +<title>github.com/prometheus/procfs->strings</title> +<path fill="none" stroke="#000000" d="M3364.7246,-13776.3858C3395.8084,-13719.875 3489.0644,-13541.6403 3522,-13380.5998 3555.9577,-13214.5616 3479.4002,-10466.9871 3580,-10330.5998 3691.9601,-10178.811 3870.532,-10349.0127 3983,-10197.5998 4057.7265,-10096.9974 4035.4118,-8067.7944 4041,-7942.5998 4055.9311,-7608.0939 4086.4598,-7201.5672 4094.1899,-7100.9522"/> +<polygon fill="#000000" stroke="#000000" points="4095.9439,-7100.9669 4094.5827,-7095.8474 4092.4542,-7100.6983 4095.9439,-7100.9669"/> +</g> +<!-- github.com/prometheus/procfs->time --> +<g id="edge599" class="edge"> +<title>github.com/prometheus/procfs->time</title> +<path fill="none" stroke="#000000" d="M3413.1726,-13776.53C3543.3366,-13732.9263 3854.8688,-13608.3506 3983,-13380.5998 4069.3547,-13227.1059 4026.0805,-11972.0847 4041,-11796.5998 4053.605,-11648.3385 4081.7084,-11471.5679 4092.0673,-11408.9699"/> +<polygon fill="#000000" stroke="#000000" points="4093.8057,-11409.1835 4092.8981,-11403.9644 4090.3529,-11408.6103 4093.8057,-11409.1835"/> +</g> +<!-- github.com/prometheus/procfs->os --> +<g id="edge593" class="edge"> +<title>github.com/prometheus/procfs->os</title> +<path fill="none" stroke="#000000" d="M3361.461,-13812.9236C3388.6045,-13885.4296 3487.5377,-14159.808 3522,-14395.5998 3572.5129,-14741.2096 3474.8709,-17205.5149 3580,-17538.5998 3676.4654,-17844.2353 3875.8575,-17834.5417 3983,-18136.5998 4031.6277,-18273.692 3962.9596,-18665.8455 4041,-18788.5998 4046.4508,-18797.1738 4055.0837,-18803.7173 4063.9087,-18808.5827"/> +<polygon fill="#000000" stroke="#000000" points="4063.5667,-18810.3757 4068.8123,-18811.1153 4065.1729,-18807.2659 4063.5667,-18810.3757"/> +</g> +<!-- github.com/prometheus/procfs->path/filepath --> +<g id="edge594" class="edge"> +<title>github.com/prometheus/procfs->path/filepath</title> +<path fill="none" stroke="#000000" d="M3361.4835,-13812.9203C3388.711,-13885.4141 3487.9166,-14159.7529 3522,-14395.5998 3582.4925,-14814.1894 3493.0661,-17787.6928 3580,-18201.5998 3688.4527,-18717.9616 3794.289,-18829.2039 4041,-19295.5998 4052.7437,-19317.8007 4068.5127,-19341.6997 4080.1548,-19358.4845"/> +<polygon fill="#000000" stroke="#000000" points="4078.7248,-19359.4935 4083.0207,-19362.5932 4081.5955,-19357.4911 4078.7248,-19359.4935"/> +</g> +<!-- github.com/prometheus/procfs->regexp --> +<g id="edge595" class="edge"> +<title>github.com/prometheus/procfs->regexp</title> +<path fill="none" stroke="#000000" d="M3364.8229,-13776.4056C3396.1897,-13719.9516 3490.1737,-13541.8632 3522,-13380.5998 3551.724,-13229.9889 3474.3526,-2438.9811 3580,-2327.5998 3645.9667,-2258.0529 3961.2046,-2283.692 4063.7146,-2294.0802"/> +<polygon fill="#000000" stroke="#000000" points="4063.5699,-2295.8244 4068.7221,-2294.5927 4063.9263,-2292.3426 4063.5699,-2295.8244"/> +</g> +<!-- github.com/prometheus/procfs->net --> +<g id="edge592" class="edge"> +<title>github.com/prometheus/procfs->net</title> +<path fill="none" stroke="#000000" d="M3361.4348,-13812.9274C3388.4801,-13885.4479 3487.0956,-14159.873 3522,-14395.5998 3543.2603,-14539.1812 3491.2934,-16892.7139 3580,-17007.5998 3693.8334,-17155.0281 3807.8583,-17059.2081 3983,-17122.5998 4010.8721,-17132.688 4041.5755,-17146.452 4063.9344,-17156.985"/> +<polygon fill="#000000" stroke="#000000" points="4063.3068,-17158.624 4068.5749,-17159.1811 4064.8041,-17155.4604 4063.3068,-17158.624"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs --> +<g id="node130" class="node"> +<title>github.com/prometheus/procfs/internal/fs</title> +<g id="a_node130"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/fs" xlink:title="github.com/prometheus/procfs/internal/fs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3890.5,-18186.5998C3890.5,-18186.5998 3672.5,-18186.5998 3672.5,-18186.5998 3666.5,-18186.5998 3660.5,-18180.5998 3660.5,-18174.5998 3660.5,-18174.5998 3660.5,-18162.5998 3660.5,-18162.5998 3660.5,-18156.5998 3666.5,-18150.5998 3672.5,-18150.5998 3672.5,-18150.5998 3890.5,-18150.5998 3890.5,-18150.5998 3896.5,-18150.5998 3902.5,-18156.5998 3902.5,-18162.5998 3902.5,-18162.5998 3902.5,-18174.5998 3902.5,-18174.5998 3902.5,-18180.5998 3896.5,-18186.5998 3890.5,-18186.5998"/> +<text text-anchor="middle" x="3781.5" y="-18164.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/fs</text> +</a> +</g> +</g> +<!-- github.com/prometheus/procfs->github.com/prometheus/procfs/internal/fs --> +<g id="edge588" class="edge"> +<title>github.com/prometheus/procfs->github.com/prometheus/procfs/internal/fs</title> +<path fill="none" stroke="#000000" d="M3361.4797,-13812.9208C3388.6929,-13885.4167 3487.852,-14159.7622 3522,-14395.5998 3536.6264,-14496.6147 3525.3551,-17983.3916 3580,-18069.5998 3605.6185,-18110.0157 3653.0069,-18134.4358 3695.3934,-18148.9071"/> +<polygon fill="#000000" stroke="#000000" points="3694.9733,-18150.6118 3700.2702,-18150.5363 3696.0823,-18147.2922 3694.9733,-18150.6118"/> +</g> +<!-- github.com/prometheus/procfs/internal/util --> +<g id="node131" class="node"> +<title>github.com/prometheus/procfs/internal/util</title> +<g id="a_node131"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/util" xlink:title="github.com/prometheus/procfs/internal/util" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3895,-14085.5998C3895,-14085.5998 3668,-14085.5998 3668,-14085.5998 3662,-14085.5998 3656,-14079.5998 3656,-14073.5998 3656,-14073.5998 3656,-14061.5998 3656,-14061.5998 3656,-14055.5998 3662,-14049.5998 3668,-14049.5998 3668,-14049.5998 3895,-14049.5998 3895,-14049.5998 3901,-14049.5998 3907,-14055.5998 3907,-14061.5998 3907,-14061.5998 3907,-14073.5998 3907,-14073.5998 3907,-14079.5998 3901,-14085.5998 3895,-14085.5998"/> +<text text-anchor="middle" x="3781.5" y="-14063.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/util</text> +</a> +</g> +</g> +<!-- github.com/prometheus/procfs->github.com/prometheus/procfs/internal/util --> +<g id="edge589" class="edge"> +<title>github.com/prometheus/procfs->github.com/prometheus/procfs/internal/util</title> +<path fill="none" stroke="#000000" d="M3365.4301,-13812.9253C3394.0599,-13859.1125 3476.2495,-13980.5198 3580,-14035.5998 3601.6139,-14047.0744 3626.2369,-14054.6863 3650.6708,-14059.6846"/> +<polygon fill="#000000" stroke="#000000" points="3650.6366,-14061.4615 3655.8808,-14060.712 3651.3138,-14058.0276 3650.6366,-14061.4615"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->sort --> +<g id="edge571" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->sort</title> +<path fill="none" stroke="#000000" d="M3800.3061,-5333.5242C3841.1383,-5293.0499 3937.8572,-5190.2834 3983,-5083.5998 4047.6946,-4930.7106 4011.6526,-4877.9988 4041,-4714.5998 4057.6064,-4622.1394 4080.8229,-4512.7312 4090.9464,-4465.831"/> +<polygon fill="#000000" stroke="#000000" points="4092.7114,-4465.9485 4092.0574,-4460.6916 4089.2904,-4465.2089 4092.7114,-4465.9485"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strconv --> +<g id="edge572" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strconv</title> +<path fill="none" stroke="#000000" d="M3983.3466,-5351.5998C4014.0035,-5351.5998 4042.1951,-5351.5998 4062.7083,-5351.5998"/> +<polygon fill="#000000" stroke="#000000" points="4062.755,-5353.3499 4067.7549,-5351.5998 4062.7549,-5349.8499 4062.755,-5353.3499"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strings --> +<g id="edge573" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strings</title> +<path fill="none" stroke="#000000" d="M3956.8499,-5369.6907C3966.594,-5375.577 3975.4641,-5382.7927 3983,-5391.5998 4034.476,-5451.7593 4031.2219,-6737.0292 4041,-6815.5998 4052.109,-6904.8643 4077.8569,-7008.9076 4089.7595,-7054.363"/> +<polygon fill="#000000" stroke="#000000" points="4088.1078,-7054.9631 4091.0724,-7059.3533 4091.4926,-7054.0725 4088.1078,-7054.9631"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->fmt --> +<g id="edge600" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->fmt</title> +<path fill="none" stroke="#000000" d="M3785.4228,-18150.4356C3809.7109,-18036.9624 3939.9507,-17413.5725 3983,-16897.5998 4040.908,-16203.5371 4027.6368,-11325.9459 4041,-10629.5998 4055.69,-9864.1158 4089.334,-8924.4714 4095.1392,-8765.0862"/> +<polygon fill="#000000" stroke="#000000" points="4096.9004,-8764.8076 4095.3339,-8759.7471 4093.4027,-8764.68 4096.9004,-8764.8076"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->os --> +<g id="edge601" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->os</title> +<path fill="none" stroke="#000000" d="M3819.765,-18186.7662C3866.8559,-18211.3334 3945.2069,-18259.8752 3983,-18326.5998 4084.9909,-18506.6668 3926.8337,-18615.9953 4041,-18788.5998 4046.605,-18797.0738 4055.2812,-18803.5892 4064.0986,-18808.4596"/> +<polygon fill="#000000" stroke="#000000" points="4063.7487,-18810.2495 4068.993,-18810.9981 4065.3602,-18807.1425 4063.7487,-18810.2495"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->path/filepath --> +<g id="edge602" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->path/filepath</title> +<path fill="none" stroke="#000000" d="M3792.28,-18186.7748C3827.1122,-18246.5181 3937.0378,-18443.2252 3983,-18621.5998 4058.022,-18912.7522 3943.5515,-19011.1674 4041,-19295.5998 4048.9827,-19318.8998 4064.7049,-19342.1592 4077.3023,-19358.4416"/> +<polygon fill="#000000" stroke="#000000" points="4075.9669,-19359.5747 4080.4311,-19362.4267 4078.7198,-19357.4133 4075.9669,-19359.5747"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->bytes --> +<g id="edge603" class="edge"> +<title>github.com/prometheus/procfs/internal/util->bytes</title> +<path fill="none" stroke="#000000" d="M3907.184,-14069.062C3934.7152,-14063.8942 3961.901,-14053.8457 3983,-14035.5998 4048.4984,-13978.9585 4012.7431,-13931.4522 4041,-13849.5998 4056.0326,-13806.0547 4075.5398,-13755.9444 4086.8251,-13727.4969"/> +<polygon fill="#000000" stroke="#000000" points="4088.4782,-13728.0756 4088.6987,-13722.7827 4085.2257,-13726.7828 4088.4782,-13728.0756"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->io/ioutil --> +<g id="edge604" class="edge"> +<title>github.com/prometheus/procfs/internal/util->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3807.5068,-14085.7725C3852.7766,-14119.1193 3944.8654,-14194.9497 3983,-14284.5998 4064.9437,-14477.24 4092.3508,-16065.6828 4095.6508,-16277.3303"/> +<polygon fill="#000000" stroke="#000000" points="4093.9012,-16277.3817 4095.7286,-16282.3539 4097.4008,-16277.3274 4093.9012,-16277.3817"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->strconv --> +<g id="edge606" class="edge"> +<title>github.com/prometheus/procfs/internal/util->strconv</title> +<path fill="none" stroke="#000000" d="M3907.1787,-14073.994C3936.0671,-14069.0133 3963.8801,-14057.816 3983,-14035.5998 4049.0035,-13958.9076 4038.6095,-6851.7553 4041,-6750.5998 4054.1973,-6192.1467 4088.0237,-5508.9171 4094.8056,-5374.9885"/> +<polygon fill="#000000" stroke="#000000" points="4096.5663,-5374.8199 4095.0719,-5369.7377 4093.0708,-5374.6426 4096.5663,-5374.8199"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->strings --> +<g id="edge607" class="edge"> +<title>github.com/prometheus/procfs/internal/util->strings</title> +<path fill="none" stroke="#000000" d="M3907.1549,-14073.9735C3936.0437,-14068.9932 3963.8628,-14057.8011 3983,-14035.5998 4038.2544,-13971.4984 4037.911,-8027.1723 4041,-7942.5998 4053.2218,-7607.984 4085.7511,-7201.5384 4094.0509,-7100.9466"/> +<polygon fill="#000000" stroke="#000000" points="4095.8048,-7100.9702 4094.4729,-7095.8429 4092.3167,-7100.6817 4095.8048,-7100.9702"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->os --> +<g id="edge605" class="edge"> +<title>github.com/prometheus/procfs/internal/util->os</title> +<path fill="none" stroke="#000000" d="M3793.0588,-14085.7543C3829.7678,-14144.6 3943.5974,-14336.5277 3983,-14513.5998 4034.5922,-14745.4508 3921.1893,-18583.5095 4041,-18788.5998 4046.2677,-18797.6169 4055.1446,-18804.3381 4064.2469,-18809.2293"/> +<polygon fill="#000000" stroke="#000000" points="4063.4663,-18810.7955 4068.7186,-18811.4855 4065.0429,-18807.6707 4063.4663,-18810.7955"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->syscall --> +<g id="edge608" class="edge"> +<title>github.com/prometheus/procfs/internal/util->syscall</title> +<path fill="none" stroke="#000000" d="M3793.0523,-14085.7558C3829.7416,-14144.6059 3943.5184,-14336.5454 3983,-14513.5998 4080.0077,-14948.6283 3985.8283,-18082.3144 4041,-18524.5998 4050.665,-18602.0797 4076.134,-18691.3553 4088.7317,-18732.5629"/> +<polygon fill="#000000" stroke="#000000" points="4087.1327,-18733.3171 4090.2753,-18737.5818 4090.4781,-18732.2882 4087.1327,-18733.3171"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->fmt --> +<g id="edge635" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->fmt</title> +<path fill="none" stroke="#000000" d="M3891.5989,-13286.2479C3925.9037,-13281.4391 3960.5805,-13269.2977 3983,-13242.5998 4029.6884,-13187.0019 4039.1374,-10702.1771 4041,-10629.5998 4060.6423,-9864.2269 4090.2052,-8924.4909 4095.2541,-8765.0888"/> +<polygon fill="#000000" stroke="#000000" points="4097.014,-8764.8021 4095.4233,-8759.7491 4093.5158,-8764.6912 4097.014,-8764.8021"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->io --> +<g id="edge636" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->io</title> +<path fill="none" stroke="#000000" d="M3891.6725,-13276.6291C3925.153,-13280.5513 3959.3102,-13291.2922 3983,-13315.5998 4049.0671,-13383.3902 4017.5153,-13645.9 4041,-13737.5998 4052.2974,-13781.7124 4072.8751,-13830.8019 4085.4053,-13858.7637"/> +<polygon fill="#000000" stroke="#000000" points="4083.845,-13859.5609 4087.4958,-13863.3994 4087.0356,-13858.122 4083.845,-13859.5609"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->sync --> +<g id="edge639" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->sync</title> +<path fill="none" stroke="#000000" d="M3786.1003,-13264.3842C3812.1153,-13160.3349 3941.117,-12630.3241 3983,-12188.5998 4070.8616,-11261.9558 3996.8139,-4741.3505 4041,-3811.5998 4052.1592,-3576.792 4083.7795,-3293.5692 4093.2724,-3211.752"/> +<polygon fill="#000000" stroke="#000000" points="4095.0256,-3211.8256 4093.8652,-3206.6568 4091.549,-3211.421 4095.0256,-3211.8256"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->time --> +<g id="edge640" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->time</title> +<path fill="none" stroke="#000000" d="M3891.5891,-13285.9795C3925.787,-13281.1278 3960.4004,-13269.0283 3983,-13242.5998 4035.2511,-13181.4962 4034.0495,-11876.6967 4041,-11796.5998 4053.8636,-11648.3607 4081.8081,-11471.5765 4092.0961,-11408.9723"/> +<polygon fill="#000000" stroke="#000000" points="4093.8346,-11409.1844 4092.9211,-11403.9664 4090.3812,-11408.6152 4093.8346,-11409.1844"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->os --> +<g id="edge637" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->os</title> +<path fill="none" stroke="#000000" d="M3891.6844,-13274.7309C3925.8897,-13278.3436 3960.4833,-13289.3784 3983,-13315.5998 4082.0484,-13430.9448 3964.5049,-18657.209 4041,-18788.5998 4046.302,-18797.7068 4055.2893,-18804.47 4064.4792,-18809.3715"/> +<polygon fill="#000000" stroke="#000000" points="4063.7372,-18810.9571 4068.9917,-18811.6303 4065.3039,-18807.8273 4063.7372,-18810.9571"/> +</g> +<!-- github.com/ulikunitz/xz/internal/xlog->runtime --> +<g id="edge638" class="edge"> +<title>github.com/ulikunitz/xz/internal/xlog->runtime</title> +<path fill="none" stroke="#000000" d="M3891.6231,-13274.7836C3925.8237,-13278.4003 3960.43,-13289.4243 3983,-13315.5998 4054.7492,-13398.8111 4034.3424,-17160.9288 4041,-17270.5998 4052.5274,-17460.4906 4082.6751,-17688.33 4092.7028,-17761.0745"/> +<polygon fill="#000000" stroke="#000000" points="4091.0263,-17761.7276 4093.4448,-17766.4407 4094.4933,-17761.2481 4091.0263,-17761.7276"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->bufio --> +<g id="edge641" class="edge"> +<title>github.com/ulikunitz/xz/lzma->bufio</title> +<path fill="none" stroke="#000000" d="M3373.3402,-13365.8484C3411.4792,-13402.5737 3500.929,-13487.617 3580,-13554.5998 3778.3555,-13722.6312 3825.7094,-13768.8966 4041,-13914.5998 4048.3491,-13919.5735 4056.5015,-13924.5766 4064.2294,-13929.1009"/> +<polygon fill="#000000" stroke="#000000" points="4063.7717,-13930.8583 4068.9752,-13931.8515 4065.5268,-13927.8302 4063.7717,-13930.8583"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->bytes --> +<g id="edge642" class="edge"> +<title>github.com/ulikunitz/xz/lzma->bytes</title> +<path fill="none" stroke="#000000" d="M3443.6409,-13361.0838C3615.7948,-13387.1991 3977.6705,-13442.4882 3983,-13446.5998 4060.2956,-13506.2318 4085.7562,-13629.7648 4093.2201,-13681.2645"/> +<polygon fill="#000000" stroke="#000000" points="4091.5001,-13681.6013 4093.9268,-13686.3102 4094.9663,-13681.1157 4091.5001,-13681.6013"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->errors --> +<g id="edge643" class="edge"> +<title>github.com/ulikunitz/xz/lzma->errors</title> +<path fill="none" stroke="#000000" d="M3443.6032,-13351.7083C3472.8942,-13348.1735 3502.8279,-13338.3533 3522,-13315.5998 3635.2113,-13181.2407 3514.933,-10316.8035 3580,-10153.5998 3678.3293,-9906.9662 3884.2512,-9959.0658 3983,-9712.5998 4038.1189,-9575.0293 4024.1491,-7192.8404 4041,-7045.5998 4051.2653,-6955.9033 4077.4452,-6851.5097 4089.6089,-6805.909"/> +<polygon fill="#000000" stroke="#000000" points="4091.3465,-6806.1855 4090.9511,-6800.9029 4087.9659,-6805.2791 4091.3465,-6806.1855"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->fmt --> +<g id="edge644" class="edge"> +<title>github.com/ulikunitz/xz/lzma->fmt</title> +<path fill="none" stroke="#000000" d="M3443.9231,-13350.6755C3472.7069,-13346.9314 3502.2348,-13337.2423 3522,-13315.5998 3610.6925,-13218.483 3529.7271,-12848.1344 3580,-12726.5998 3688.0434,-12465.4054 3883.609,-12499.2077 3983,-12234.5998 3991.1235,-12212.9727 4086.1741,-9067.4046 4095.2975,-8764.908"/> +<polygon fill="#000000" stroke="#000000" points="4097.0482,-8764.9073 4095.4498,-8759.8568 4093.5498,-8764.8017 4097.0482,-8764.9073"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->io --> +<g id="edge647" class="edge"> +<title>github.com/ulikunitz/xz/lzma->io</title> +<path fill="none" stroke="#000000" d="M3400.8334,-13365.6786C3538.5283,-13419.7154 3938.4652,-13579.0218 3983,-13623.5998 4000.8582,-13641.4753 4063.5402,-13798.667 4087.0252,-13858.56"/> +<polygon fill="#000000" stroke="#000000" points="4085.4582,-13859.3578 4088.9111,-13863.3753 4088.7172,-13858.0814 4085.4582,-13859.3578"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->unicode --> +<g id="edge648" class="edge"> +<title>github.com/ulikunitz/xz/lzma->unicode</title> +<path fill="none" stroke="#000000" d="M3443.7531,-13351.8339C3473.0587,-13348.3113 3502.9633,-13338.4667 3522,-13315.5998 3681.4217,-13124.1022 3439.9157,-4551.6654 3580,-4345.5998 3693.3624,-4178.8424 3964.9835,-4145.738 4061.3593,-4139.1989"/> +<polygon fill="#000000" stroke="#000000" points="4061.4868,-4140.9444 4066.3623,-4138.8728 4061.2591,-4137.4518 4061.4868,-4140.9444"/> +</g> +<!-- github.com/ulikunitz/xz/lzma->github.com/ulikunitz/xz/internal/xlog --> +<g id="edge646" class="edge"> +<title>github.com/ulikunitz/xz/lzma->github.com/ulikunitz/xz/internal/xlog</title> +<path fill="none" stroke="#000000" d="M3443.7532,-13334.0133C3508.4556,-13324.164 3596.5079,-13310.7602 3666.4843,-13300.1081"/> +<polygon fill="#000000" stroke="#000000" points="3666.8112,-13301.8286 3671.4908,-13299.3459 3666.2844,-13298.3684 3666.8112,-13301.8286"/> +</g> +<!-- github.com/ulikunitz/xz/internal/hash --> +<g id="node135" class="node"> +<title>github.com/ulikunitz/xz/internal/hash</title> +<g id="a_node135"><a xlink:href="https://godoc.org/github.com/ulikunitz/xz/internal/hash" xlink:title="github.com/ulikunitz/xz/internal/hash" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3880,-13365.5998C3880,-13365.5998 3683,-13365.5998 3683,-13365.5998 3677,-13365.5998 3671,-13359.5998 3671,-13353.5998 3671,-13353.5998 3671,-13341.5998 3671,-13341.5998 3671,-13335.5998 3677,-13329.5998 3683,-13329.5998 3683,-13329.5998 3880,-13329.5998 3880,-13329.5998 3886,-13329.5998 3892,-13335.5998 3892,-13341.5998 3892,-13341.5998 3892,-13353.5998 3892,-13353.5998 3892,-13359.5998 3886,-13365.5998 3880,-13365.5998"/> +<text text-anchor="middle" x="3781.5" y="-13343.8998" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/ulikunitz/xz/internal/hash</text> +</a> +</g> +</g> +<!-- github.com/ulikunitz/xz/lzma->github.com/ulikunitz/xz/internal/hash --> +<g id="edge645" class="edge"> +<title>github.com/ulikunitz/xz/lzma->github.com/ulikunitz/xz/internal/hash</title> +<path fill="none" stroke="#000000" d="M3443.7532,-13347.5998C3508.0879,-13347.5998 3595.5079,-13347.5998 3665.2896,-13347.5998"/> +<polygon fill="#000000" stroke="#000000" points="3665.7069,-13349.3499 3670.7069,-13347.5998 3665.7068,-13345.8499 3665.7069,-13349.3499"/> +</g> +<!-- golang.org/x/net/internal/socks->context --> +<g id="edge649" class="edge"> +<title>golang.org/x/net/internal/socks->context</title> +<path fill="none" stroke="#000000" d="M3809.3753,-7835.5038C3855.7598,-7803.5258 3947.2212,-7732.069 3983,-7644.5998 4047.7007,-7486.4245 4031.8037,-1663.2487 4041,-1492.5998 4052.2031,-1284.7136 4083.0735,-1034.7296 4092.9314,-958.0753"/> +<polygon fill="#000000" stroke="#000000" points="4094.7002,-958.0415 4093.6043,-952.8587 4091.2289,-957.5937 4094.7002,-958.0415"/> +</g> +<!-- golang.org/x/net/internal/socks->errors --> +<g id="edge650" class="edge"> +<title>golang.org/x/net/internal/socks->errors</title> +<path fill="none" stroke="#000000" d="M3808.2041,-7835.4106C3853.2612,-7803.0329 3943.3416,-7730.6573 3983,-7644.5998 4094.9437,-7401.6855 4000.6756,-7310.0099 4041,-7045.5998 4054.6451,-6956.1278 4079.1997,-6851.2069 4090.2892,-6805.6485"/> +<polygon fill="#000000" stroke="#000000" points="4092.0237,-6805.9223 4091.5099,-6800.6499 4088.6236,-6805.0919 4092.0237,-6805.9223"/> +</g> +<!-- golang.org/x/net/internal/socks->io --> +<g id="edge651" class="edge"> +<title>golang.org/x/net/internal/socks->io</title> +<path fill="none" stroke="#000000" d="M3786.9591,-7871.7945C3814.7537,-7965.4669 3940.8659,-8403.1995 3983,-8771.5998 4045.7026,-9319.8412 3937.7267,-13195.5344 4041,-13737.5998 4049.5222,-13782.3315 4071.0139,-13831.2171 4084.4691,-13858.9726"/> +<polygon fill="#000000" stroke="#000000" points="4082.9503,-13859.8503 4086.7196,-13863.5726 4086.0942,-13858.3122 4082.9503,-13859.8503"/> +</g> +<!-- golang.org/x/net/internal/socks->strconv --> +<g id="edge653" class="edge"> +<title>golang.org/x/net/internal/socks->strconv</title> +<path fill="none" stroke="#000000" d="M3809.0028,-7835.3471C3854.8318,-7803.1354 3945.4872,-7731.3395 3983,-7644.5998 4006.7793,-7589.6156 4085.5982,-5614.5079 4095.0668,-5375.2504"/> +<polygon fill="#000000" stroke="#000000" points="4096.8284,-5374.9886 4095.2775,-5369.9233 4093.3311,-5374.8502 4096.8284,-5374.9886"/> +</g> +<!-- golang.org/x/net/internal/socks->time --> +<g id="edge654" class="edge"> +<title>golang.org/x/net/internal/socks->time</title> +<path fill="none" stroke="#000000" d="M3786.884,-7871.8033C3814.3078,-7965.5191 3938.8808,-8403.432 3983,-8771.5998 4000.0718,-8914.0612 3968.0754,-11230.0333 4041,-11353.5998 4046.3077,-11362.5934 4055.1951,-11369.3084 4064.2947,-11374.2012"/> +<polygon fill="#000000" stroke="#000000" points="4063.512,-11375.7664 4068.7641,-11376.4588 4065.0901,-11372.6424 4063.512,-11375.7664"/> +</g> +<!-- golang.org/x/net/internal/socks->net --> +<g id="edge652" class="edge"> +<title>golang.org/x/net/internal/socks->net</title> +<path fill="none" stroke="#000000" d="M3786.9922,-7871.7907C3814.9502,-7965.4447 3941.7407,-8403.1005 3983,-8771.5998 3995.9339,-8887.1169 3982.7002,-17040.0383 4041,-17140.5998 4046.2853,-17149.7165 4055.2683,-17156.4821 4064.4595,-17161.383"/> +<polygon fill="#000000" stroke="#000000" points="4063.7183,-17162.969 4068.973,-17163.6412 4065.2845,-17159.8389 4063.7183,-17162.969"/> +</g> +</g> +</svg> diff --git a/images/crane.png b/images/crane.png Binary files differnew file mode 100644 index 0000000..ffd95af --- /dev/null +++ b/images/crane.png diff --git a/images/credhelper-basic.svg b/images/credhelper-basic.svg new file mode 100644 index 0000000..44d4d0e --- /dev/null +++ b/images/credhelper-basic.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" width="1948" height="812" xmlns:xlink="http://www.w3.org/1999/xlink"><desc style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Created with Raphaël 2.2.0</desc><defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><marker id="raphael-marker-endblock55-objbv9so" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objlilim" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objy0dfc" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objcudj2" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objv9tph" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objl72wc" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj7gt9b" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objhtahn" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objay9je" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objaok9x" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objhhdsz" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objv9d0m" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker></defs><rect x="10" y="10" width="346.0625" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="15" y="15" width="336.0625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="183.03125" y="24" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Credential helper flow - Basic auth</tspan></text><rect x="10" y="48" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="58" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><rect x="10" y="754.09375" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="764.09375" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><path fill="none" stroke="#000000" d="M39.40625,86L39.40625,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="578.484375" y="48" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="588.6875" y="58" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="627.09375" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><rect x="578.484375" y="754.09375" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="588.6875" y="764.09375" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="627.09375" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><path fill="none" stroke="#000000" d="M627.09375,86L627.09375,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="954.9609375" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="965.15625" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="993.96875" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><rect x="954.9609375" y="754.09375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="965.15625" y="764.09375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="993.96875" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><path fill="none" stroke="#000000" d="M993.96875,86L993.96875,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1369.0390625" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1379.234375" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1408.046875" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><rect x="1369.0390625" y="754.09375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1379.234375" y="764.09375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1408.046875" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,86L1408.046875,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1840.515625" y="48" width="77.625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1850.515625" y="58" width="57.625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1879.328125" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">gcloud</tspan></text><rect x="1840.515625" y="754.09375" width="77.625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1850.515625" y="764.09375" width="57.625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1879.328125" y="773.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">gcloud</tspan></text><path fill="none" stroke="#000000" d="M1879.328125,86L1879.328125,754.09375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="294.84375" y="102" width="76.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="111" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/</tspan></text><path fill="none" stroke="#000000" d="M39.40625,124C39.40625,124,565.1157077997923,124,622.101791604262,124" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objbv9so)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="73" y="130.390625" width="519.5" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="149" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">401 Unauthorized</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Www-Authenticate: Bearer realm="<rlm>",service="<svc>"</tspan></text><path fill="none" stroke="#000000" d="M627.09375,181.21875C627.09375,181.21875,101.38429220020771,181.21875,44.39820839573804,181.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objlilim)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="406.265625" y="197.21875" width="220.84375" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="516.6875" y="206.21875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GetAuthConfig("gcr.io")</tspan></text><path fill="none" stroke="#000000" d="M39.40625,219.21875C39.40625,219.21875,914.81398099754,219.21875,988.9757199272633,219.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objy0dfc)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="647.09375" y="239.21875" width="326.875" height="47.21875" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="652.09375" y="244.21875" width="316.875" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="810.53125" y="262.828125" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">~/.docker/config.json:</tspan><tspan dy="19.2" x="810.53125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"credHelpers":{"gcr.io": "gcr"}}</tspan></text><rect x="1004.171875" y="302.4375" width="394.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1201.0078125" y="311.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ echo gcr.io | docker-credential-gcr get</tspan></text><path fill="none" stroke="#000000" d="M993.96875,324.4375C993.96875,324.4375,1356.106105241226,324.4375,1403.0531110110237,324.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objcudj2)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1422.84375" y="340.4375" width="441.6875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1643.6875" y="349.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ gcloud auth print-access-token --format=json</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,362.4375C1408.046875,362.4375,1823.878506992478,362.4375,1874.3346400735963,362.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objv9tph)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1418.046875" y="378.4375" width="451.28125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1643.6875" y="387.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"access_token":"hunter2","token_expiry":"..."}</tspan></text><path fill="none" stroke="#000000" d="M1879.328125,400.4375C1879.328125,400.4375,1463.496493007522,400.4375,1413.0403599264037,400.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objl72wc)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1008.96875" y="416.4375" width="384.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1201.0078125" y="425.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"Username":"_token","Secret":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1408.046875,438.4375C1408.046875,438.4375,1045.909519758774,438.4375,998.9625139889764,438.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj7gt9b)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="315.046875" y="454.4375" width="403.28125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="516.6875" y="463.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"username":"_token","password":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M993.96875,476.4375C993.96875,476.4375,118.56101900245994,476.4375,44.3992800727367,476.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objhtahn)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="59.40625" y="496.4375" width="547.6875" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="64.40625" y="501.4375" width="537.6875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="510.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">note: base64("_token:hunter2") == "X3Rva2VuOmh1bnRlcjI="</tspan></text><rect x="135.40625" y="530.828125" width="395.078125" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="549.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET <rlm>?service=<svc>&scope=...</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Basic X3Rva2VuOmh1bnRlcjI=</tspan></text><path fill="none" stroke="#000000" d="M39.40625,581.65625C39.40625,581.65625,565.1157077997923,581.65625,622.101791604262,581.65625" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objay9je)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="208.421875" y="588.046875" width="249.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="606.65625" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">200 OK</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"token":"<bearer token>"}</tspan></text><path fill="none" stroke="#000000" d="M627.09375,638.875C627.09375,638.875,101.38429220020771,638.875,44.39820839573804,638.875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objaok9x)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="159.421875" y="645.265625" width="346.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="663.875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/_catalog</tspan><tspan dy="19.2" x="333.25" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Bearer <bearer token></tspan></text><path fill="none" stroke="#000000" d="M39.40625,696.09375C39.40625,696.09375,565.1157077997923,696.09375,622.101791604262,696.09375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objhhdsz)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="184.421875" y="712.09375" width="297.65625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="333.25" y="721.09375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"repositories":["foo", "bar"]}</tspan></text><path fill="none" stroke="#000000" d="M627.09375,734.09375C627.09375,734.09375,101.38429220020771,734.09375,44.39820839573804,734.09375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objv9d0m)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg>
\ No newline at end of file diff --git a/images/credhelper-oauth.svg b/images/credhelper-oauth.svg new file mode 100644 index 0000000..a88e1b8 --- /dev/null +++ b/images/credhelper-oauth.svg @@ -0,0 +1 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"><svg xmlns="http://www.w3.org/2000/svg" width="1763" height="852" xmlns:xlink="http://www.w3.org/1999/xlink"><desc style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Created with Raphaël 2.2.0</desc><defs style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><marker id="raphael-marker-endblock55-objujk3m" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj2bf7s" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-obj6hbu7" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objtv5cq" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objcypdu" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objviw31" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objn0c01" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objyeoik" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objdivb3" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker><marker id="raphael-marker-endblock55-objrg426" markerHeight="5" markerWidth="5" orient="auto" refX="2.5" refY="2.5" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"><use xlink:href="#raphael-marker-block" transform="rotate(180 2.5 2.5) scale(1,1)" stroke-width="1.0000" fill="#000" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></use></marker></defs><rect x="10" y="10" width="298.0625" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="15" y="15" width="288.0625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="159.03125" y="24" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Credential helper flow - Oauth</tspan></text><rect x="10" y="48" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="58" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><rect x="10" y="794.484375" width="58.8125" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="20.203125" y="804.484375" width="38.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="39.40625" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">ggcr</tspan></text><path fill="none" stroke="#000000" d="M39.40625,86L39.40625,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="586.890625" y="48" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="597.09375" y="58" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="635.5" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><rect x="586.890625" y="794.484375" width="97.21875" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="597.09375" y="804.484375" width="77.21875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="635.5" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">registry</tspan></text><path fill="none" stroke="#000000" d="M635.5,86L635.5,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1193.8046875" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1204" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1232.8125" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><rect x="1193.8046875" y="794.484375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1204" y="804.484375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1232.8125" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">config</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,86L1232.8125,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1655.8828125" y="48" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1666.078125" y="58" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1694.890625" y="67" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><rect x="1655.8828125" y="794.484375" width="78.015625" height="38" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="1666.078125" y="804.484375" width="58.015625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1694.890625" y="813.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">helper</tspan></text><path fill="none" stroke="#000000" d="M1694.890625,86L1694.890625,794.484375" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="299.046875" y="102" width="76.8125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="111" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/</tspan></text><path fill="none" stroke="#000000" d="M39.40625,124C39.40625,124,573.0484363525175,124,630.5035643265068,124" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objujk3m)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="77.203125" y="130.390625" width="519.5" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="149" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">401 Unauthorized</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Www-Authenticate: Bearer realm="<rlm>",service="<svc>"</tspan></text><path fill="none" stroke="#000000" d="M635.5,181.21875C635.5,181.21875,101.85781364748254,181.21875,44.40268567349324,181.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj2bf7s)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="501.6875" y="197.21875" width="268.84375" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="636.109375" y="206.21875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GetAuthConfig("example.com")</tspan></text><path fill="none" stroke="#000000" d="M39.40625,219.21875C39.40625,219.21875,1144.2001858446747,219.21875,1227.8149612626066,219.21875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-obj6hbu7)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="837.9375" y="239.21875" width="374.875" height="47.21875" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="842.9375" y="244.21875" width="364.875" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1025.375" y="262.828125" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">~/.docker/config.json:</tspan><tspan dy="19.2" x="1025.375" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"credHelpers":{"example.com": "foo"}}</tspan></text><rect x="1243.015625" y="302.4375" width="442.078125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1463.8515625" y="311.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">$ echo example.com | docker-credential-foo get</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,324.4375C1232.8125,324.4375,1639.9941534968093,324.4375,1689.897764350178,324.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objtv5cq)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="1267.015625" y="340.4375" width="393.671875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="1463.8515625" y="349.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"Username":"<token>","Secret":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1694.890625,362.4375C1694.890625,362.4375,1287.7089715031907,362.4375,1237.805360649822,362.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objcypdu)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="655.5" y="382.4375" width="557.3125" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="660.5" y="387.4375" width="547.3125" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="934.15625" y="396.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">the "<token>" username indicates this is an IdentityToken</tspan></text><rect x="506.484375" y="426.4375" width="259.25" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="636.109375" y="435.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"identitytoken":"hunter2"}</tspan></text><path fill="none" stroke="#000000" d="M1232.8125,448.4375C1232.8125,448.4375,128.0185641553253,448.4375,44.40378873739337,448.4375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objviw31)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="59.40625" y="468.4375" width="470.875" height="28" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="64.40625" y="473.4375" width="460.875" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="294.84375" y="482.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">the IdentityToken indicates we should use oauth2</tspan></text><rect x="59.40625" y="516.4375" width="10" height="10" rx="0" ry="0" fill="none" stroke="#000000" stroke-width="2" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><rect x="0" y="0" width="0" height="0" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="64.40625" y="521.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="521.4375" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></tspan></text><rect x="49.40625" y="513.625" width="576.09375" height="75.609375" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="551.4375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-22.8046875" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">POST <rlm></tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"> </tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">service=<svc>&grant_type=refresh_token&refresh_token=hunter2</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">&client_id=go-containerregistry&scope=...</tspan></text><path fill="none" stroke="#000000" d="M39.40625,622.046875C39.40625,622.046875,573.0484363525175,622.046875,630.5035643265068,622.046875" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objn0c01)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="212.625" y="628.4375" width="249.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="647.046875" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">200 OK</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"token":"<bearer token>"}</tspan></text><path fill="none" stroke="#000000" d="M635.5,679.265625C635.5,679.265625,101.85781364748254,679.265625,44.40268567349324,679.265625" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objyeoik)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="163.625" y="685.65625" width="346.65625" height="37.21875" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="704.265625" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="-3.6015625" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">GET /v2/_catalog</tspan><tspan dy="19.2" x="337.453125" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">Authorization: Bearer <bearer token></tspan></text><path fill="none" stroke="#000000" d="M39.40625,736.484375C39.40625,736.484375,573.0484363525175,736.484375,630.5035643265068,736.484375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objdivb3)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path><rect x="188.625" y="752.484375" width="297.65625" height="18" rx="0" ry="0" fill="#ffffff" stroke="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></rect><text x="337.453125" y="761.484375" text-anchor="middle" font-family="Andale Mono, monospace" font-size="16px" stroke="none" fill="#000000" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); text-anchor: middle; font-family: "Andale Mono", monospace; font-size: 16px;"><tspan dy="6" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">{"repositories":["foo", "bar"]}</tspan></text><path fill="none" stroke="#000000" d="M635.5,774.484375C635.5,774.484375,101.85781364748254,774.484375,44.40268567349324,774.484375" stroke-width="2" marker-end="url(#raphael-marker-endblock55-objrg426)" stroke-dasharray="none" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path></svg>
\ No newline at end of file diff --git a/images/docker.dot.svg b/images/docker.dot.svg new file mode 100644 index 0000000..f031ddf --- /dev/null +++ b/images/docker.dot.svg @@ -0,0 +1,2155 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: godep Pages: 1 --> +<svg width="9165pt" height="1078pt" + viewBox="0.00 0.00 9165.00 1078.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 1074)"> +<title>godep</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-1074 9161,-1074 9161,4 -4,4"/> +<!-- bufio --> +<g id="node1" class="node"> +<title>bufio</title> +<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4088,-36C4088,-36 4058,-36 4058,-36 4052,-36 4046,-30 4046,-24 4046,-24 4046,-12 4046,-12 4046,-6 4052,0 4058,0 4058,0 4088,0 4088,0 4094,0 4100,-6 4100,-12 4100,-12 4100,-24 4100,-24 4100,-30 4094,-36 4088,-36"/> +<text text-anchor="middle" x="4073" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text> +</a> +</g> +</g> +<!-- bytes --> +<g id="node2" class="node"> +<title>bytes</title> +<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M805,-36C805,-36 775,-36 775,-36 769,-36 763,-30 763,-24 763,-24 763,-12 763,-12 763,-6 769,0 775,0 775,0 805,0 805,0 811,0 817,-6 817,-12 817,-12 817,-24 817,-24 817,-30 811,-36 805,-36"/> +<text text-anchor="middle" x="790" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text> +</a> +</g> +</g> +<!-- compress/gzip --> +<g id="node3" class="node"> +<title>compress/gzip</title> +<g id="a_node3"><a xlink:href="https://godoc.org/compress/gzip" xlink:title="compress/gzip" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5451.5,-412C5451.5,-412 5380.5,-412 5380.5,-412 5374.5,-412 5368.5,-406 5368.5,-400 5368.5,-400 5368.5,-388 5368.5,-388 5368.5,-382 5374.5,-376 5380.5,-376 5380.5,-376 5451.5,-376 5451.5,-376 5457.5,-376 5463.5,-382 5463.5,-388 5463.5,-388 5463.5,-400 5463.5,-400 5463.5,-406 5457.5,-412 5451.5,-412"/> +<text text-anchor="middle" x="5416" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">compress/gzip</text> +</a> +</g> +</g> +<!-- context --> +<g id="node4" class="node"> +<title>context</title> +<g id="a_node4"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M8369,-224C8369,-224 8337,-224 8337,-224 8331,-224 8325,-218 8325,-212 8325,-212 8325,-200 8325,-200 8325,-194 8331,-188 8337,-188 8337,-188 8369,-188 8369,-188 8375,-188 8381,-194 8381,-200 8381,-200 8381,-212 8381,-212 8381,-218 8375,-224 8369,-224"/> +<text text-anchor="middle" x="8353" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text> +</a> +</g> +</g> +<!-- crypto --> +<g id="node5" class="node"> +<title>crypto</title> +<g id="a_node5"><a xlink:href="https://godoc.org/crypto" xlink:title="crypto" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7623,-36C7623,-36 7593,-36 7593,-36 7587,-36 7581,-30 7581,-24 7581,-24 7581,-12 7581,-12 7581,-6 7587,0 7593,0 7593,0 7623,0 7623,0 7629,0 7635,-6 7635,-12 7635,-12 7635,-24 7635,-24 7635,-30 7629,-36 7623,-36"/> +<text text-anchor="middle" x="7608" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto</text> +</a> +</g> +</g> +<!-- crypto/tls --> +<g id="node6" class="node"> +<title>crypto/tls</title> +<g id="a_node6"><a xlink:href="https://godoc.org/crypto/tls" xlink:title="crypto/tls" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5549,-412C5549,-412 5505,-412 5505,-412 5499,-412 5493,-406 5493,-400 5493,-400 5493,-388 5493,-388 5493,-382 5499,-376 5505,-376 5505,-376 5549,-376 5549,-376 5555,-376 5561,-382 5561,-388 5561,-388 5561,-400 5561,-400 5561,-406 5555,-412 5549,-412"/> +<text text-anchor="middle" x="5527" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">crypto/tls</text> +</a> +</g> +</g> +<!-- encoding --> +<g id="node7" class="node"> +<title>encoding</title> +<g id="a_node7"><a xlink:href="https://godoc.org/encoding" xlink:title="encoding" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2929,-36C2929,-36 2887,-36 2887,-36 2881,-36 2875,-30 2875,-24 2875,-24 2875,-12 2875,-12 2875,-6 2881,0 2887,0 2887,0 2929,0 2929,0 2935,0 2941,-6 2941,-12 2941,-12 2941,-24 2941,-24 2941,-30 2935,-36 2929,-36"/> +<text text-anchor="middle" x="2908" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding</text> +</a> +</g> +</g> +<!-- encoding/binary --> +<g id="node8" class="node"> +<title>encoding/binary</title> +<g id="a_node8"><a xlink:href="https://godoc.org/encoding/binary" xlink:title="encoding/binary" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1605,-36C1605,-36 1525,-36 1525,-36 1519,-36 1513,-30 1513,-24 1513,-24 1513,-12 1513,-12 1513,-6 1519,0 1525,0 1525,0 1605,0 1605,0 1611,0 1617,-6 1617,-12 1617,-12 1617,-24 1617,-24 1617,-30 1611,-36 1605,-36"/> +<text text-anchor="middle" x="1565" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/binary</text> +</a> +</g> +</g> +<!-- encoding/hex --> +<g id="node9" class="node"> +<title>encoding/hex</title> +<g id="a_node9"><a xlink:href="https://godoc.org/encoding/hex" xlink:title="encoding/hex" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2647.5,-130C2647.5,-130 2582.5,-130 2582.5,-130 2576.5,-130 2570.5,-124 2570.5,-118 2570.5,-118 2570.5,-106 2570.5,-106 2570.5,-100 2576.5,-94 2582.5,-94 2582.5,-94 2647.5,-94 2647.5,-94 2653.5,-94 2659.5,-100 2659.5,-106 2659.5,-106 2659.5,-118 2659.5,-118 2659.5,-124 2653.5,-130 2647.5,-130"/> +<text text-anchor="middle" x="2615" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/hex</text> +</a> +</g> +</g> +<!-- encoding/json --> +<g id="node10" class="node"> +<title>encoding/json</title> +<g id="a_node10"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M352,-36C352,-36 284,-36 284,-36 278,-36 272,-30 272,-24 272,-24 272,-12 272,-12 272,-6 278,0 284,0 284,0 352,0 352,0 358,0 364,-6 364,-12 364,-12 364,-24 364,-24 364,-30 358,-36 352,-36"/> +<text text-anchor="middle" x="318" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text> +</a> +</g> +</g> +<!-- errors --> +<g id="node11" class="node"> +<title>errors</title> +<g id="a_node11"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4528,-36C4528,-36 4498,-36 4498,-36 4492,-36 4486,-30 4486,-24 4486,-24 4486,-12 4486,-12 4486,-6 4492,0 4498,0 4498,0 4528,0 4528,0 4534,0 4540,-6 4540,-12 4540,-12 4540,-24 4540,-24 4540,-30 4534,-36 4528,-36"/> +<text text-anchor="middle" x="4513" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text> +</a> +</g> +</g> +<!-- expvar --> +<g id="node12" class="node"> +<title>expvar</title> +<g id="a_node12"><a xlink:href="https://godoc.org/expvar" xlink:title="expvar" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3309,-318C3309,-318 3279,-318 3279,-318 3273,-318 3267,-312 3267,-306 3267,-306 3267,-294 3267,-294 3267,-288 3273,-282 3279,-282 3279,-282 3309,-282 3309,-282 3315,-282 3321,-288 3321,-294 3321,-294 3321,-306 3321,-306 3321,-312 3315,-318 3309,-318"/> +<text text-anchor="middle" x="3294" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">expvar</text> +</a> +</g> +</g> +<!-- fmt --> +<g id="node13" class="node"> +<title>fmt</title> +<g id="a_node13"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M6145,-36C6145,-36 6115,-36 6115,-36 6109,-36 6103,-30 6103,-24 6103,-24 6103,-12 6103,-12 6103,-6 6109,0 6115,0 6115,0 6145,0 6145,0 6151,0 6157,-6 6157,-12 6157,-12 6157,-24 6157,-24 6157,-30 6151,-36 6145,-36"/> +<text text-anchor="middle" x="6130" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text> +</a> +</g> +</g> +<!-- github.com/beorn7/perks/quantile --> +<g id="node14" class="node"> +<title>github.com/beorn7/perks/quantile</title> +<g id="a_node14"><a xlink:href="https://godoc.org/github.com/beorn7/perks/quantile" xlink:title="github.com/beorn7/perks/quantile" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2293,-130C2293,-130 2117,-130 2117,-130 2111,-130 2105,-124 2105,-118 2105,-118 2105,-106 2105,-106 2105,-100 2111,-94 2117,-94 2117,-94 2293,-94 2293,-94 2299,-94 2305,-100 2305,-106 2305,-106 2305,-118 2305,-118 2305,-124 2299,-130 2293,-130"/> +<text text-anchor="middle" x="2205" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/beorn7/perks/quantile</text> +</a> +</g> +</g> +<!-- math --> +<g id="node15" class="node"> +<title>math</title> +<g id="a_node15"><a xlink:href="https://godoc.org/math" xlink:title="math" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2469,-36C2469,-36 2439,-36 2439,-36 2433,-36 2427,-30 2427,-24 2427,-24 2427,-12 2427,-12 2427,-6 2433,0 2439,0 2439,0 2469,0 2469,0 2475,0 2481,-6 2481,-12 2481,-12 2481,-24 2481,-24 2481,-30 2475,-36 2469,-36"/> +<text text-anchor="middle" x="2454" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math</text> +</a> +</g> +</g> +<!-- github.com/beorn7/perks/quantile->math --> +<g id="edge1" class="edge"> +<title>github.com/beorn7/perks/quantile->math</title> +<path fill="none" stroke="#000000" d="M2252.7151,-93.9871C2302.4813,-75.1998 2379.1023,-46.2746 2422.0308,-30.0687"/> +<polygon fill="#000000" stroke="#000000" points="2422.7808,-31.6562 2426.8405,-28.253 2421.5447,-28.3817 2422.7808,-31.6562"/> +</g> +<!-- sort --> +<g id="node16" class="node"> +<title>sort</title> +<g id="a_node16"><a xlink:href="https://godoc.org/sort" xlink:title="sort" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2834,-36C2834,-36 2804,-36 2804,-36 2798,-36 2792,-30 2792,-24 2792,-24 2792,-12 2792,-12 2792,-6 2798,0 2804,0 2804,0 2834,0 2834,0 2840,0 2846,-6 2846,-12 2846,-12 2846,-24 2846,-24 2846,-30 2840,-36 2834,-36"/> +<text text-anchor="middle" x="2819" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sort</text> +</a> +</g> +</g> +<!-- github.com/beorn7/perks/quantile->sort --> +<g id="edge2" class="edge"> +<title>github.com/beorn7/perks/quantile->sort</title> +<path fill="none" stroke="#000000" d="M2305.0162,-96.6881C2446.4494,-75.0354 2697.5888,-36.5874 2786.6104,-22.9587"/> +<polygon fill="#000000" stroke="#000000" points="2787.0578,-24.6607 2791.7353,-22.1741 2786.528,-21.201 2787.0578,-24.6607"/> +</g> +<!-- github.com/cespare/xxhash/v2 --> +<g id="node17" class="node"> +<title>github.com/cespare/xxhash/v2</title> +<g id="a_node17"><a xlink:href="https://godoc.org/github.com/cespare/xxhash/v2" xlink:title="github.com/cespare/xxhash/v2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1769,-130C1769,-130 1611,-130 1611,-130 1605,-130 1599,-124 1599,-118 1599,-118 1599,-106 1599,-106 1599,-100 1605,-94 1611,-94 1611,-94 1769,-94 1769,-94 1775,-94 1781,-100 1781,-106 1781,-106 1781,-118 1781,-118 1781,-124 1775,-130 1769,-130"/> +<text text-anchor="middle" x="1690" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/cespare/xxhash/v2</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->encoding/binary --> +<g id="edge3" class="edge"> +<title>github.com/cespare/xxhash/v2->encoding/binary</title> +<path fill="none" stroke="#000000" d="M1665.8987,-93.8759C1645.1475,-78.2709 1615.1757,-55.7321 1593.2912,-39.275"/> +<polygon fill="#000000" stroke="#000000" points="1594.0752,-37.675 1589.0273,-36.0685 1591.9716,-40.4723 1594.0752,-37.675"/> +</g> +<!-- github.com/cespare/xxhash/v2->errors --> +<g id="edge4" class="edge"> +<title>github.com/cespare/xxhash/v2->errors</title> +<path fill="none" stroke="#000000" d="M1781.3307,-107.4655C1862.3942,-103.542 1984.0619,-97.9012 2090,-94 2989.3152,-60.8822 3214.6866,-69.1684 4114,-36 4250.4202,-30.9686 4412.9886,-23.0305 4480.8084,-19.632"/> +<polygon fill="#000000" stroke="#000000" points="4481.008,-21.3742 4485.9139,-19.3757 4480.8325,-17.8786 4481.008,-21.3742"/> +</g> +<!-- math/bits --> +<g id="node18" class="node"> +<title>math/bits</title> +<g id="a_node18"><a xlink:href="https://godoc.org/math/bits" xlink:title="math/bits" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1707,-36C1707,-36 1663,-36 1663,-36 1657,-36 1651,-30 1651,-24 1651,-24 1651,-12 1651,-12 1651,-6 1657,0 1663,0 1663,0 1707,0 1707,0 1713,0 1719,-6 1719,-12 1719,-12 1719,-24 1719,-24 1719,-30 1713,-36 1707,-36"/> +<text text-anchor="middle" x="1685" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/bits</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->math/bits --> +<g id="edge5" class="edge"> +<title>github.com/cespare/xxhash/v2->math/bits</title> +<path fill="none" stroke="#000000" d="M1689.0359,-93.8759C1688.2405,-78.9211 1687.1063,-57.5983 1686.2427,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="1687.9743,-40.9685 1685.9611,-36.0685 1684.4792,-41.1544 1687.9743,-40.9685"/> +</g> +<!-- reflect --> +<g id="node19" class="node"> +<title>reflect</title> +<g id="a_node19"><a xlink:href="https://godoc.org/reflect" xlink:title="reflect" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1873,-36C1873,-36 1843,-36 1843,-36 1837,-36 1831,-30 1831,-24 1831,-24 1831,-12 1831,-12 1831,-6 1837,0 1843,0 1843,0 1873,0 1873,0 1879,0 1885,-6 1885,-12 1885,-12 1885,-24 1885,-24 1885,-30 1879,-36 1873,-36"/> +<text text-anchor="middle" x="1858" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">reflect</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->reflect --> +<g id="edge6" class="edge"> +<title>github.com/cespare/xxhash/v2->reflect</title> +<path fill="none" stroke="#000000" d="M1722.3921,-93.8759C1752.35,-77.1137 1796.6065,-52.3511 1826.3314,-35.7193"/> +<polygon fill="#000000" stroke="#000000" points="1827.464,-37.091 1830.9729,-33.1223 1825.755,-34.0366 1827.464,-37.091"/> +</g> +<!-- unsafe --> +<g id="node20" class="node"> +<title>unsafe</title> +<g id="a_node20"><a xlink:href="https://godoc.org/unsafe" xlink:title="unsafe" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1790,-36C1790,-36 1760,-36 1760,-36 1754,-36 1748,-30 1748,-24 1748,-24 1748,-12 1748,-12 1748,-6 1754,0 1760,0 1760,0 1790,0 1790,0 1796,0 1802,-6 1802,-12 1802,-12 1802,-24 1802,-24 1802,-30 1796,-36 1790,-36"/> +<text text-anchor="middle" x="1775" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unsafe</text> +</a> +</g> +</g> +<!-- github.com/cespare/xxhash/v2->unsafe --> +<g id="edge7" class="edge"> +<title>github.com/cespare/xxhash/v2->unsafe</title> +<path fill="none" stroke="#000000" d="M1706.3889,-93.8759C1720.2645,-78.531 1740.2029,-56.4815 1755.0134,-40.1029"/> +<polygon fill="#000000" stroke="#000000" points="1756.6059,-40.9509 1758.6615,-36.0685 1754.0099,-38.6034 1756.6059,-40.9509"/> +</g> +<!-- github.com/docker/distribution --> +<g id="node21" class="node"> +<title>github.com/docker/distribution</title> +<g id="a_node21"><a xlink:href="https://godoc.org/github.com/docker/distribution" xlink:title="github.com/docker/distribution" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6693.5,-412C6693.5,-412 6532.5,-412 6532.5,-412 6526.5,-412 6520.5,-406 6520.5,-400 6520.5,-400 6520.5,-388 6520.5,-388 6520.5,-382 6526.5,-376 6532.5,-376 6532.5,-376 6693.5,-376 6693.5,-376 6699.5,-376 6705.5,-382 6705.5,-388 6705.5,-388 6705.5,-400 6705.5,-400 6705.5,-406 6699.5,-412 6693.5,-412"/> +<text text-anchor="middle" x="6613" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->context --> +<g id="edge8" class="edge"> +<title>github.com/docker/distribution->context</title> +<path fill="none" stroke="#000000" d="M6705.5837,-392.1741C6925.99,-387.0345 7476.5567,-369.3105 7653,-318 7685.4594,-308.5607 7687.6671,-291.8639 7720,-282 7969.0581,-206.0189 8049.0205,-281.4237 8303,-224 8308.5118,-222.7538 8314.2483,-221.0736 8319.7865,-219.2383"/> +<polygon fill="#000000" stroke="#000000" points="8320.6428,-220.7947 8324.8047,-217.5172 8319.5073,-217.484 8320.6428,-220.7947"/> +</g> +<!-- github.com/docker/distribution->errors --> +<g id="edge9" class="edge"> +<title>github.com/docker/distribution->errors</title> +<path fill="none" stroke="#000000" d="M6520.4294,-392.4731C6252.9184,-387.4723 5491.5163,-368.8824 5392,-318 5277.2879,-259.348 5321.3097,-155.3174 5208,-94 5091.2424,-30.8168 4666.57,-20.1208 4545.4495,-18.3458"/> +<polygon fill="#000000" stroke="#000000" points="4545.2752,-16.5933 4540.2509,-18.2724 4545.2257,-20.0929 4545.2752,-16.5933"/> +</g> +<!-- github.com/docker/distribution->fmt --> +<g id="edge10" class="edge"> +<title>github.com/docker/distribution->fmt</title> +<path fill="none" stroke="#000000" d="M6616.9833,-375.8966C6621.2183,-353.5354 6626.3718,-314.4714 6619,-282 6598.4911,-191.6622 6602.3936,-150.5235 6529,-94 6471.2677,-49.538 6246.7,-27.3321 6162.3022,-20.4254"/> +<polygon fill="#000000" stroke="#000000" points="6162.3131,-18.6706 6157.1882,-20.0116 6162.0307,-22.1592 6162.3131,-18.6706"/> +</g> +<!-- github.com/docker/distribution/reference --> +<g id="node22" class="node"> +<title>github.com/docker/distribution/reference</title> +<g id="a_node22"><a xlink:href="https://godoc.org/github.com/docker/distribution/reference" xlink:title="github.com/docker/distribution/reference" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7574.5,-318C7574.5,-318 7359.5,-318 7359.5,-318 7353.5,-318 7347.5,-312 7347.5,-306 7347.5,-306 7347.5,-294 7347.5,-294 7347.5,-288 7353.5,-282 7359.5,-282 7359.5,-282 7574.5,-282 7574.5,-282 7580.5,-282 7586.5,-288 7586.5,-294 7586.5,-294 7586.5,-306 7586.5,-306 7586.5,-312 7580.5,-318 7574.5,-318"/> +<text text-anchor="middle" x="7467" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/reference</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->github.com/docker/distribution/reference --> +<g id="edge11" class="edge"> +<title>github.com/docker/distribution->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M6705.6112,-383.8063C6859.7555,-366.8396 7169.0821,-332.7919 7342.2637,-313.7298"/> +<polygon fill="#000000" stroke="#000000" points="7342.5689,-315.4568 7347.3474,-313.1702 7342.1859,-311.9778 7342.5689,-315.4568"/> +</g> +<!-- github.com/opencontainers/go-digest --> +<g id="node23" class="node"> +<title>github.com/opencontainers/go-digest</title> +<g id="a_node23"><a xlink:href="https://godoc.org/github.com/opencontainers/go-digest" xlink:title="github.com/opencontainers/go-digest" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7753,-130C7753,-130 7559,-130 7559,-130 7553,-130 7547,-124 7547,-118 7547,-118 7547,-106 7547,-106 7547,-100 7553,-94 7559,-94 7559,-94 7753,-94 7753,-94 7759,-94 7765,-100 7765,-106 7765,-106 7765,-118 7765,-118 7765,-124 7759,-130 7753,-130"/> +<text text-anchor="middle" x="7656" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/go-digest</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->github.com/opencontainers/go-digest --> +<g id="edge12" class="edge"> +<title>github.com/docker/distribution->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M6705.5897,-389.7839C6935.2198,-378.9154 7518.3865,-348.6443 7601,-318 7626.3366,-308.6017 7627.8222,-297.4407 7650,-282 7688.4426,-255.2354 7715.7886,-265.2411 7738,-224 7756.2432,-190.1269 7719.8418,-154.9442 7689.8566,-133.2589"/> +<polygon fill="#000000" stroke="#000000" points="7690.5771,-131.6244 7685.4864,-130.1591 7688.5522,-134.4792 7690.5771,-131.6244"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="node24" class="node"> +<title>github.com/opencontainers/image-spec/specs-go/v1</title> +<g id="a_node24"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" xlink:title="github.com/opencontainers/image-spec/specs-go/v1" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6401.5,-224C6401.5,-224 6126.5,-224 6126.5,-224 6120.5,-224 6114.5,-218 6114.5,-212 6114.5,-212 6114.5,-200 6114.5,-200 6114.5,-194 6120.5,-188 6126.5,-188 6126.5,-188 6401.5,-188 6401.5,-188 6407.5,-188 6413.5,-194 6413.5,-200 6413.5,-200 6413.5,-212 6413.5,-212 6413.5,-218 6407.5,-224 6401.5,-224"/> +<text text-anchor="middle" x="6264" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go/v1</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->github.com/opencontainers/image-spec/specs-go/v1 --> +<g id="edge13" class="edge"> +<title>github.com/docker/distribution->github.com/opencontainers/image-spec/specs-go/v1</title> +<path fill="none" stroke="#000000" d="M6614.0421,-375.7202C6614.3943,-350.9542 6611.0931,-306.9736 6586,-282 6560.207,-256.3299 6481.0625,-237.4523 6407.6988,-224.932"/> +<polygon fill="#000000" stroke="#000000" points="6407.649,-223.1488 6402.4274,-224.0415 6407.066,-226.5999 6407.649,-223.1488"/> +</g> +<!-- io --> +<g id="node25" class="node"> +<title>io</title> +<g id="a_node25"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5024,-36C5024,-36 4994,-36 4994,-36 4988,-36 4982,-30 4982,-24 4982,-24 4982,-12 4982,-12 4982,-6 4988,0 4994,0 4994,0 5024,0 5024,0 5030,0 5036,-6 5036,-12 5036,-12 5036,-24 5036,-24 5036,-30 5030,-36 5024,-36"/> +<text text-anchor="middle" x="5009" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->io --> +<g id="edge14" class="edge"> +<title>github.com/docker/distribution->io</title> +<path fill="none" stroke="#000000" d="M6605.7455,-375.6873C6594.5383,-350.0333 6570.4296,-304.0688 6534,-282 6367.5556,-181.169 6290.1931,-265.197 6100,-224 6045.9963,-212.3025 6034.7095,-200.9816 5981,-188 5754.8419,-133.3374 5695.2107,-133.9748 5466,-94 5306.797,-66.2347 5115.9782,-35.2359 5041.2458,-23.1835"/> +<polygon fill="#000000" stroke="#000000" points="5041.2825,-21.4169 5036.0677,-22.3488 5040.7254,-24.8723 5041.2825,-21.4169"/> +</g> +<!-- mime --> +<g id="node26" class="node"> +<title>mime</title> +<g id="a_node26"><a xlink:href="https://godoc.org/mime" xlink:title="mime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4599,-224C4599,-224 4569,-224 4569,-224 4563,-224 4557,-218 4557,-212 4557,-212 4557,-200 4557,-200 4557,-194 4563,-188 4569,-188 4569,-188 4599,-188 4599,-188 4605,-188 4611,-194 4611,-200 4611,-200 4611,-212 4611,-212 4611,-218 4605,-224 4599,-224"/> +<text text-anchor="middle" x="4584" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">mime</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->mime --> +<g id="edge15" class="edge"> +<title>github.com/docker/distribution->mime</title> +<path fill="none" stroke="#000000" d="M6520.3415,-391.2925C6236.3874,-382.6853 5380.8895,-354.3643 5105,-318 4917.6389,-293.3044 4697.572,-236.7125 4616.2775,-214.8478"/> +<polygon fill="#000000" stroke="#000000" points="4616.4085,-213.0707 4611.1253,-213.4586 4615.4973,-216.45 4616.4085,-213.0707"/> +</g> +<!-- net/http --> +<g id="node27" class="node"> +<title>net/http</title> +<g id="a_node27"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5955,-224C5955,-224 5921,-224 5921,-224 5915,-224 5909,-218 5909,-212 5909,-212 5909,-200 5909,-200 5909,-194 5915,-188 5921,-188 5921,-188 5955,-188 5955,-188 5961,-188 5967,-194 5967,-200 5967,-200 5967,-212 5967,-212 5967,-218 5961,-224 5955,-224"/> +<text text-anchor="middle" x="5938" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->net/http --> +<g id="edge16" class="edge"> +<title>github.com/docker/distribution->net/http</title> +<path fill="none" stroke="#000000" d="M6520.3804,-386.8659C6427.0148,-377.7546 6279.7073,-358.0894 6158,-318 6148.3047,-314.8064 6031.1713,-254.3237 5971.899,-223.5994"/> +<polygon fill="#000000" stroke="#000000" points="5972.6316,-222.0081 5967.3872,-221.2603 5971.0206,-225.1153 5972.6316,-222.0081"/> +</g> +<!-- strings --> +<g id="node28" class="node"> +<title>strings</title> +<g id="a_node28"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7075,-36C7075,-36 7045,-36 7045,-36 7039,-36 7033,-30 7033,-24 7033,-24 7033,-12 7033,-12 7033,-6 7039,0 7045,0 7045,0 7075,0 7075,0 7081,0 7087,-6 7087,-12 7087,-12 7087,-24 7087,-24 7087,-30 7081,-36 7075,-36"/> +<text text-anchor="middle" x="7060" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->strings --> +<g id="edge17" class="edge"> +<title>github.com/docker/distribution->strings</title> +<path fill="none" stroke="#000000" d="M6635.2487,-375.786C6653.7862,-360.5228 6680.8086,-338.0697 6704,-318 6769.4481,-261.3617 6783.0554,-244.0596 6849,-188 6913.4944,-133.1732 6991.5716,-71.4298 7032.2719,-39.5817"/> +<polygon fill="#000000" stroke="#000000" points="7033.7087,-40.6797 7036.5695,-36.2212 7031.5527,-37.9225 7033.7087,-40.6797"/> +</g> +<!-- time --> +<g id="node29" class="node"> +<title>time</title> +<g id="a_node29"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M4953,-130C4953,-130 4923,-130 4923,-130 4917,-130 4911,-124 4911,-118 4911,-118 4911,-106 4911,-106 4911,-100 4917,-94 4923,-94 4923,-94 4953,-94 4953,-94 4959,-94 4965,-100 4965,-106 4965,-106 4965,-118 4965,-118 4965,-124 4959,-130 4953,-130"/> +<text text-anchor="middle" x="4938" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution->time --> +<g id="edge18" class="edge"> +<title>github.com/docker/distribution->time</title> +<path fill="none" stroke="#000000" d="M6520.3333,-391.959C6229.1255,-385.1255 5344.5304,-361.0234 5223,-318 5109.9253,-277.9701 5002.5421,-178.0437 4958.7191,-133.7393"/> +<polygon fill="#000000" stroke="#000000" points="4959.9293,-132.4741 4955.1751,-130.1373 4957.4344,-134.9288 4959.9293,-132.4741"/> +</g> +<!-- github.com/docker/distribution/reference->errors --> +<g id="edge25" class="edge"> +<title>github.com/docker/distribution/reference->errors</title> +<path fill="none" stroke="#000000" d="M7406.7341,-281.867C7366.235,-268.5201 7312.3622,-248.4752 7268,-224 7244.6512,-211.1181 7244.7952,-197.8136 7220,-188 7002.1325,-101.7708 6927.2705,-167.6259 6696,-130 6621.0581,-117.8076 6604.3375,-103.4449 6529,-94 5839.6952,-7.5832 5661.1893,-62.6497 4967,-36 4808.8146,-29.9273 4619.6416,-22.3141 4545.2745,-19.3075"/> +<polygon fill="#000000" stroke="#000000" points="4545.1871,-17.5526 4540.1204,-19.099 4545.0456,-21.0497 4545.1871,-17.5526"/> +</g> +<!-- github.com/docker/distribution/reference->fmt --> +<g id="edge26" class="edge"> +<title>github.com/docker/distribution/reference->fmt</title> +<path fill="none" stroke="#000000" d="M7440.9296,-281.9347C7404.4573,-257.4177 7335.4229,-213.7246 7271,-188 7108.2233,-123.0019 7060.7385,-123.7054 6888,-94 6609.2088,-46.057 6267.1448,-25.174 6162.1526,-19.601"/> +<polygon fill="#000000" stroke="#000000" points="6162.1245,-17.8472 6157.0394,-19.3318 6161.9404,-21.3423 6162.1245,-17.8472"/> +</g> +<!-- github.com/docker/distribution/reference->github.com/opencontainers/go-digest --> +<g id="edge28" class="edge"> +<title>github.com/docker/distribution/reference->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M7565.2656,-281.9741C7625.6485,-268.6737 7694.8229,-248.6404 7713,-224 7734.269,-195.1682 7704.7972,-157.2402 7680.9298,-133.7493"/> +<polygon fill="#000000" stroke="#000000" points="7681.9689,-132.3201 7677.156,-130.1068 7679.5382,-134.8384 7681.9689,-132.3201"/> +</g> +<!-- github.com/docker/distribution/reference->strings --> +<g id="edge31" class="edge"> +<title>github.com/docker/distribution/reference->strings</title> +<path fill="none" stroke="#000000" d="M7560.865,-281.9369C7625.7529,-264.9496 7694.9391,-234.9962 7662,-188 7644.4288,-162.9301 7432.3958,-102.5517 7403,-94 7290.4969,-61.2712 7153.6983,-34.8667 7092.415,-23.7242"/> +<polygon fill="#000000" stroke="#000000" points="7092.6596,-21.9901 7087.4278,-22.8208 7092.0358,-25.4341 7092.6596,-21.9901"/> +</g> +<!-- github.com/docker/distribution/digestset --> +<g id="node30" class="node"> +<title>github.com/docker/distribution/digestset</title> +<g id="a_node30"><a xlink:href="https://godoc.org/github.com/docker/distribution/digestset" xlink:title="github.com/docker/distribution/digestset" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7635,-224C7635,-224 7423,-224 7423,-224 7417,-224 7411,-218 7411,-212 7411,-212 7411,-200 7411,-200 7411,-194 7417,-188 7423,-188 7423,-188 7635,-188 7635,-188 7641,-188 7647,-194 7647,-200 7647,-200 7647,-212 7647,-212 7647,-218 7641,-224 7635,-224"/> +<text text-anchor="middle" x="7529" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/digestset</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/reference->github.com/docker/distribution/digestset --> +<g id="edge27" class="edge"> +<title>github.com/docker/distribution/reference->github.com/docker/distribution/digestset</title> +<path fill="none" stroke="#000000" d="M7478.9542,-281.8759C7488.9895,-266.661 7503.3724,-244.8548 7514.1461,-228.5205"/> +<polygon fill="#000000" stroke="#000000" points="7515.7903,-229.2059 7517.0825,-224.0685 7512.8686,-227.2788 7515.7903,-229.2059"/> +</g> +<!-- path --> +<g id="node34" class="node"> +<title>path</title> +<g id="a_node34"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7193,-224C7193,-224 7163,-224 7163,-224 7157,-224 7151,-218 7151,-212 7151,-212 7151,-200 7151,-200 7151,-194 7157,-188 7163,-188 7163,-188 7193,-188 7193,-188 7199,-188 7205,-194 7205,-200 7205,-200 7205,-212 7205,-212 7205,-218 7199,-224 7193,-224"/> +<text text-anchor="middle" x="7178" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/reference->path --> +<g id="edge29" class="edge"> +<title>github.com/docker/distribution/reference->path</title> +<path fill="none" stroke="#000000" d="M7407.0827,-281.8527C7358.3817,-266.9043 7288.0011,-244.8508 7227,-224 7221.4392,-222.0993 7215.5517,-220.0066 7209.8598,-217.9411"/> +<polygon fill="#000000" stroke="#000000" points="7210.433,-216.2875 7205.136,-216.2173 7209.2331,-219.5754 7210.433,-216.2875"/> +</g> +<!-- regexp --> +<g id="node35" class="node"> +<title>regexp</title> +<g id="a_node35"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7824,-36C7824,-36 7794,-36 7794,-36 7788,-36 7782,-30 7782,-24 7782,-24 7782,-12 7782,-12 7782,-6 7788,0 7794,0 7794,0 7824,0 7824,0 7830,0 7836,-6 7836,-12 7836,-12 7836,-24 7836,-24 7836,-30 7830,-36 7824,-36"/> +<text text-anchor="middle" x="7809" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/reference->regexp --> +<g id="edge30" class="edge"> +<title>github.com/docker/distribution/reference->regexp</title> +<path fill="none" stroke="#000000" d="M7586.7955,-286.8266C7759.5871,-267.5455 8059.1084,-232.9542 8067,-224 8105.2019,-180.6539 8101.6048,-140.2686 8067,-94 8039.73,-57.5384 7904.2853,-32.5244 7841.3035,-22.6762"/> +<polygon fill="#000000" stroke="#000000" points="7841.3764,-20.9167 7836.1676,-21.8817 7840.8412,-24.3756 7841.3764,-20.9167"/> +</g> +<!-- github.com/opencontainers/go-digest->crypto --> +<g id="edge140" class="edge"> +<title>github.com/opencontainers/go-digest->crypto</title> +<path fill="none" stroke="#000000" d="M7646.7451,-93.8759C7639.0423,-78.7911 7628.0308,-57.227 7619.7143,-40.9405"/> +<polygon fill="#000000" stroke="#000000" points="7621.059,-39.7257 7617.2265,-36.0685 7617.9418,-41.3174 7621.059,-39.7257"/> +</g> +<!-- github.com/opencontainers/go-digest->fmt --> +<g id="edge141" class="edge"> +<title>github.com/opencontainers/go-digest->fmt</title> +<path fill="none" stroke="#000000" d="M7546.9353,-103.8141C7503.4128,-100.6263 7452.8431,-97.021 7407,-94 6911.966,-61.3777 6308.5566,-27.8212 6162.8092,-19.7986"/> +<polygon fill="#000000" stroke="#000000" points="6162.5037,-18.0293 6157.4151,-19.5019 6162.3114,-21.524 6162.5037,-18.0293"/> +</g> +<!-- github.com/opencontainers/go-digest->io --> +<g id="edge143" class="edge"> +<title>github.com/opencontainers/go-digest->io</title> +<path fill="none" stroke="#000000" d="M7546.8837,-108.1251C7085.1022,-91.7263 5303.2835,-28.4506 5041.2535,-19.1454"/> +<polygon fill="#000000" stroke="#000000" points="5041.1187,-17.3896 5036.0598,-18.9609 5040.9945,-20.8874 5041.1187,-17.3896"/> +</g> +<!-- github.com/opencontainers/go-digest->strings --> +<g id="edge145" class="edge"> +<title>github.com/opencontainers/go-digest->strings</title> +<path fill="none" stroke="#000000" d="M7546.6844,-94.759C7408.1067,-72.9027 7177.0514,-36.4611 7092.3736,-23.1059"/> +<polygon fill="#000000" stroke="#000000" points="7092.4612,-21.3482 7087.2496,-22.2978 7091.9159,-24.8054 7092.4612,-21.3482"/> +</g> +<!-- github.com/opencontainers/go-digest->regexp --> +<g id="edge144" class="edge"> +<title>github.com/opencontainers/go-digest->regexp</title> +<path fill="none" stroke="#000000" d="M7685.4999,-93.8759C7711.8499,-77.687 7750.3462,-54.0356 7777.3359,-37.4537"/> +<polygon fill="#000000" stroke="#000000" points="7778.5559,-38.7582 7781.9,-34.6497 7776.7237,-35.776 7778.5559,-38.7582"/> +</g> +<!-- hash --> +<g id="node56" class="node"> +<title>hash</title> +<g id="a_node56"><a xlink:href="https://godoc.org/hash" xlink:title="hash" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7706,-36C7706,-36 7676,-36 7676,-36 7670,-36 7664,-30 7664,-24 7664,-24 7664,-12 7664,-12 7664,-6 7670,0 7676,0 7676,0 7706,0 7706,0 7712,0 7718,-6 7718,-12 7718,-12 7718,-24 7718,-24 7718,-30 7712,-36 7706,-36"/> +<text text-anchor="middle" x="7691" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">hash</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/go-digest->hash --> +<g id="edge142" class="edge"> +<title>github.com/opencontainers/go-digest->hash</title> +<path fill="none" stroke="#000000" d="M7662.7484,-93.8759C7668.365,-78.7911 7676.3942,-57.227 7682.4583,-40.9405"/> +<polygon fill="#000000" stroke="#000000" points="7684.1676,-41.3649 7684.2724,-36.0685 7680.8876,-40.1436 7684.1676,-41.3649"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest --> +<g id="edge147" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M6413.6291,-195.8957C6693.6303,-176.9876 7291.0624,-136.6438 7541.6456,-119.7222"/> +<polygon fill="#000000" stroke="#000000" points="7541.8997,-121.4591 7546.7704,-119.3761 7541.6638,-117.9671 7541.8997,-121.4591"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->time --> +<g id="edge149" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->time</title> +<path fill="none" stroke="#000000" d="M6114.4685,-195.3997C5808.5651,-173.7143 5126.8992,-125.391 4970.4381,-114.2995"/> +<polygon fill="#000000" stroke="#000000" points="4970.2066,-112.5288 4965.0953,-113.9208 4969.959,-116.0201 4970.2066,-112.5288"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go --> +<g id="node57" class="node"> +<title>github.com/opencontainers/image-spec/specs-go</title> +<g id="a_node57"><a xlink:href="https://godoc.org/github.com/opencontainers/image-spec/specs-go" xlink:title="github.com/opencontainers/image-spec/specs-go" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6502.5,-130C6502.5,-130 6245.5,-130 6245.5,-130 6239.5,-130 6233.5,-124 6233.5,-118 6233.5,-118 6233.5,-106 6233.5,-106 6233.5,-100 6239.5,-94 6245.5,-94 6245.5,-94 6502.5,-94 6502.5,-94 6508.5,-94 6514.5,-100 6514.5,-106 6514.5,-106 6514.5,-118 6514.5,-118 6514.5,-124 6508.5,-130 6502.5,-130"/> +<text text-anchor="middle" x="6374" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/opencontainers/image-spec/specs-go</text> +</a> +</g> +</g> +<!-- github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go --> +<g id="edge148" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go/v1->github.com/opencontainers/image-spec/specs-go</title> +<path fill="none" stroke="#000000" d="M6285.2091,-187.8759C6303.318,-172.401 6329.4063,-150.1073 6348.6208,-133.6877"/> +<polygon fill="#000000" stroke="#000000" points="6350.1917,-134.6472 6352.856,-130.0685 6347.9179,-131.9864 6350.1917,-134.6472"/> +</g> +<!-- github.com/docker/distribution/digestset->errors --> +<g id="edge19" class="edge"> +<title>github.com/docker/distribution/digestset->errors</title> +<path fill="none" stroke="#000000" d="M7421.6081,-187.9866C7263.771,-162.3142 6959.6262,-115.8923 6699,-94 5931.4937,-29.5303 5736.6948,-64.1455 4967,-36 4808.8038,-30.2152 4619.6369,-22.4385 4545.2729,-19.3478"/> +<polygon fill="#000000" stroke="#000000" points="4545.1876,-17.5928 4540.1191,-19.1335 4545.0421,-21.0898 4545.1876,-17.5928"/> +</g> +<!-- github.com/docker/distribution/digestset->sort --> +<g id="edge21" class="edge"> +<title>github.com/docker/distribution/digestset->sort</title> +<path fill="none" stroke="#000000" d="M7410.9367,-197.964C7353.3686,-194.3414 7283.0967,-190.3561 7220,-188 6437.5504,-158.7821 4475.4323,-214.4058 3697,-130 3616.7575,-121.2993 3598.8813,-105.5562 3519,-94 3228.0082,-51.903 3144.8269,-108.9521 2860,-36 2857.176,-35.2767 2854.2995,-34.379 2851.4466,-33.3737"/> +<polygon fill="#000000" stroke="#000000" points="2851.6494,-31.5806 2846.3532,-31.462 2850.4195,-34.8574 2851.6494,-31.5806"/> +</g> +<!-- github.com/docker/distribution/digestset->github.com/opencontainers/go-digest --> +<g id="edge20" class="edge"> +<title>github.com/docker/distribution/digestset->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M7553.4869,-187.8759C7574.658,-172.2059 7605.2754,-149.5442 7627.5336,-133.0696"/> +<polygon fill="#000000" stroke="#000000" points="7628.6105,-134.4498 7631.5883,-130.0685 7626.5282,-131.6365 7628.6105,-134.4498"/> +</g> +<!-- github.com/docker/distribution/digestset->strings --> +<g id="edge22" class="edge"> +<title>github.com/docker/distribution/digestset->strings</title> +<path fill="none" stroke="#000000" d="M7505.0949,-187.9664C7471.6114,-163.4861 7408.0883,-119.8354 7348,-94 7259.1775,-55.8101 7146.4764,-32.865 7092.1545,-23.2675"/> +<polygon fill="#000000" stroke="#000000" points="7092.3094,-21.5181 7087.0826,-22.3803 7091.7062,-24.9658 7092.3094,-21.5181"/> +</g> +<!-- sync --> +<g id="node31" class="node"> +<title>sync</title> +<g id="a_node31"><a xlink:href="https://godoc.org/sync" xlink:title="sync" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M8558,-36C8558,-36 8528,-36 8528,-36 8522,-36 8516,-30 8516,-24 8516,-24 8516,-12 8516,-12 8516,-6 8522,0 8528,0 8528,0 8558,0 8558,0 8564,0 8570,-6 8570,-12 8570,-12 8570,-24 8570,-24 8570,-30 8564,-36 8558,-36"/> +<text text-anchor="middle" x="8543" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/digestset->sync --> +<g id="edge23" class="edge"> +<title>github.com/docker/distribution/digestset->sync</title> +<path fill="none" stroke="#000000" d="M7588.7356,-187.9155C7690.7637,-157.0685 7889.2006,-97.2691 7905,-94 8134.4217,-46.5297 8416.834,-25.7956 8510.8632,-19.8798"/> +<polygon fill="#000000" stroke="#000000" points="8511.1111,-21.6178 8515.9925,-19.5601 8510.8933,-18.1246 8511.1111,-21.6178"/> +</g> +<!-- github.com/docker/distribution/metrics --> +<g id="node32" class="node"> +<title>github.com/docker/distribution/metrics</title> +<g id="a_node32"><a xlink:href="https://godoc.org/github.com/docker/distribution/metrics" xlink:title="github.com/docker/distribution/metrics" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6019,-694C6019,-694 5813,-694 5813,-694 5807,-694 5801,-688 5801,-682 5801,-682 5801,-670 5801,-670 5801,-664 5807,-658 5813,-658 5813,-658 6019,-658 6019,-658 6025,-658 6031,-664 6031,-670 6031,-670 6031,-682 6031,-682 6031,-688 6025,-694 6019,-694"/> +<text text-anchor="middle" x="5916" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/metrics</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics --> +<g id="node33" class="node"> +<title>github.com/docker/go-metrics</title> +<g id="a_node33"><a xlink:href="https://godoc.org/github.com/docker/go-metrics" xlink:title="github.com/docker/go-metrics" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5960,-600C5960,-600 5802,-600 5802,-600 5796,-600 5790,-594 5790,-588 5790,-588 5790,-576 5790,-576 5790,-570 5796,-564 5802,-564 5802,-564 5960,-564 5960,-564 5966,-564 5972,-570 5972,-576 5972,-576 5972,-588 5972,-588 5972,-594 5966,-600 5960,-600"/> +<text text-anchor="middle" x="5881" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/go-metrics</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/metrics->github.com/docker/go-metrics --> +<g id="edge24" class="edge"> +<title>github.com/docker/distribution/metrics->github.com/docker/go-metrics</title> +<path fill="none" stroke="#000000" d="M5909.2516,-657.8759C5903.635,-642.7911 5895.6058,-621.227 5889.5417,-604.9405"/> +<polygon fill="#000000" stroke="#000000" points="5891.1124,-604.1436 5887.7276,-600.0685 5887.8324,-605.3649 5891.1124,-604.1436"/> +</g> +<!-- github.com/docker/go-metrics->fmt --> +<g id="edge103" class="edge"> +<title>github.com/docker/go-metrics->fmt</title> +<path fill="none" stroke="#000000" d="M5874.9505,-563.8632C5855.9294,-503.1349 5803.772,-304.2362 5894,-188 5950.4895,-115.2274 6026.7132,-193.9764 6093,-130 6117.3484,-106.5002 6125.6798,-66.6723 6128.5269,-41.3871"/> +<polygon fill="#000000" stroke="#000000" points="6130.2872,-41.3755 6129.055,-36.2233 6126.8053,-41.0193 6130.2872,-41.3755"/> +</g> +<!-- github.com/docker/go-metrics->net/http --> +<g id="edge106" class="edge"> +<title>github.com/docker/go-metrics->net/http</title> +<path fill="none" stroke="#000000" d="M5888.4924,-563.9037C5894.4645,-548.7105 5902.5407,-526.2942 5907,-506 5929.2808,-404.6008 5935.6441,-280.3104 5937.3809,-229.4143"/> +<polygon fill="#000000" stroke="#000000" points="5939.1386,-229.2022 5937.5528,-224.1478 5935.6405,-229.0879 5939.1386,-229.2022"/> +</g> +<!-- github.com/docker/go-metrics->time --> +<g id="edge108" class="edge"> +<title>github.com/docker/go-metrics->time</title> +<path fill="none" stroke="#000000" d="M5789.9809,-581.3241C5565.3278,-578.734 4998.3975,-566.2356 4940,-506 4839.766,-402.6111 4903.0298,-202.83 4928.6745,-135.1022"/> +<polygon fill="#000000" stroke="#000000" points="4930.4433,-135.3769 4930.5986,-130.0817 4927.1751,-134.1243 4930.4433,-135.3769"/> +</g> +<!-- github.com/docker/go-metrics->sync --> +<g id="edge107" class="edge"> +<title>github.com/docker/go-metrics->sync</title> +<path fill="none" stroke="#000000" d="M5972.1728,-577.0539C6422.8471,-552.1051 8400.7638,-436.5879 8647,-318 8711.0044,-287.1754 8758.5854,-256.566 8740,-188 8727.3367,-141.2822 8719.8703,-127.5705 8685,-94 8652.9727,-63.1665 8605.9543,-41.3172 8574.9198,-29.1858"/> +<polygon fill="#000000" stroke="#000000" points="8575.3758,-27.4862 8570.0809,-27.3223 8574.1179,-30.7523 8575.3758,-27.4862"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus --> +<g id="node49" class="node"> +<title>github.com/prometheus/client_golang/prometheus</title> +<g id="a_node49"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus" xlink:title="github.com/prometheus/client_golang/prometheus" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3024.5,-412C3024.5,-412 2757.5,-412 2757.5,-412 2751.5,-412 2745.5,-406 2745.5,-400 2745.5,-400 2745.5,-388 2745.5,-388 2745.5,-382 2751.5,-376 2757.5,-376 2757.5,-376 3024.5,-376 3024.5,-376 3030.5,-376 3036.5,-382 3036.5,-388 3036.5,-388 3036.5,-400 3036.5,-400 3036.5,-406 3030.5,-412 3024.5,-412"/> +<text text-anchor="middle" x="2891" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus --> +<g id="edge104" class="edge"> +<title>github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus</title> +<path fill="none" stroke="#000000" d="M5789.8826,-576.2709C5364.8855,-549.5487 3575.558,-437.0424 3041.8689,-403.4861"/> +<polygon fill="#000000" stroke="#000000" points="3041.9095,-401.7353 3036.8095,-403.168 3041.6898,-405.2284 3041.9095,-401.7353"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp --> +<g id="node50" class="node"> +<title>github.com/prometheus/client_golang/prometheus/promhttp</title> +<g id="a_node50"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" xlink:title="github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5287.5,-506C5287.5,-506 4966.5,-506 4966.5,-506 4960.5,-506 4954.5,-500 4954.5,-494 4954.5,-494 4954.5,-482 4954.5,-482 4954.5,-476 4960.5,-470 4966.5,-470 4966.5,-470 5287.5,-470 5287.5,-470 5293.5,-470 5299.5,-476 5299.5,-482 5299.5,-482 5299.5,-494 5299.5,-494 5299.5,-500 5293.5,-506 5287.5,-506"/> +<text text-anchor="middle" x="5127" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/promhttp</text> +</a> +</g> +</g> +<!-- github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus/promhttp --> +<g id="edge105" class="edge"> +<title>github.com/docker/go-metrics->github.com/prometheus/client_golang/prometheus/promhttp</title> +<path fill="none" stroke="#000000" d="M5789.6746,-570.6146C5662.3875,-554.7459 5430.0859,-525.7852 5277.1228,-506.7156"/> +<polygon fill="#000000" stroke="#000000" points="5276.8933,-504.9235 5271.7152,-506.0414 5276.4602,-508.3966 5276.8933,-504.9235"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode --> +<g id="node36" class="node"> +<title>github.com/docker/distribution/registry/api/errcode</title> +<g id="a_node36"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/errcode" xlink:title="github.com/docker/distribution/registry/api/errcode" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6507.5,-318C6507.5,-318 6236.5,-318 6236.5,-318 6230.5,-318 6224.5,-312 6224.5,-306 6224.5,-306 6224.5,-294 6224.5,-294 6224.5,-288 6230.5,-282 6236.5,-282 6236.5,-282 6507.5,-282 6507.5,-282 6513.5,-282 6519.5,-288 6519.5,-294 6519.5,-294 6519.5,-306 6519.5,-306 6519.5,-312 6513.5,-318 6507.5,-318"/> +<text text-anchor="middle" x="6372" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/errcode</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->encoding/json --> +<g id="edge32" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->encoding/json</title> +<path fill="none" stroke="#000000" d="M6224.4556,-295.397C6095.7644,-291.4828 5904.0399,-285.8936 5737,-282 4326.481,-249.1217 3973.0793,-272.1797 2563,-224 1728.2195,-195.4772 1508.8369,-273.5672 686,-130 570.035,-109.7666 439.2954,-64.297 369.107,-37.9368"/> +<polygon fill="#000000" stroke="#000000" points="369.4551,-36.1979 364.1591,-36.0725 368.221,-39.4731 369.4551,-36.1979"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->fmt --> +<g id="edge33" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->fmt</title> +<path fill="none" stroke="#000000" d="M6256.5107,-281.997C6191.5395,-269.2519 6119.5997,-249.8267 6100,-224 6090.3276,-211.2546 6094.282,-202.9434 6100,-188 6112.3726,-155.6653 6139.6274,-162.3347 6152,-130 6163.3056,-100.4539 6151.955,-64.2026 6141.8088,-41.0991"/> +<polygon fill="#000000" stroke="#000000" points="6143.327,-40.2091 6139.6706,-36.3758 6140.1385,-41.6526 6143.327,-40.2091"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sort --> +<g id="edge35" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sort</title> +<path fill="none" stroke="#000000" d="M6224.4503,-295.6124C6095.7558,-291.8345 5904.0293,-286.3244 5737,-282 5462.0166,-274.8806 3527.015,-297.7127 3262,-224 3230.9304,-215.3581 3227.2259,-203.598 3199,-188 3121.3832,-145.108 3104.1092,-129.8523 3023,-94 2952.6703,-62.9125 2931.4721,-64.3625 2860,-36 2857.0601,-34.8333 2854.0189,-33.5828 2850.9831,-32.3046"/> +<polygon fill="#000000" stroke="#000000" points="2851.518,-30.6304 2846.2321,-30.2812 2850.1466,-33.8505 2851.518,-30.6304"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->net/http --> +<g id="edge34" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->net/http</title> +<path fill="none" stroke="#000000" d="M6259.1444,-281.9923C6182.3344,-268.6684 6078.7299,-248.6009 5989,-224 5983.523,-222.4984 5977.7871,-220.7015 5972.2204,-218.8328"/> +<polygon fill="#000000" stroke="#000000" points="5972.4653,-217.0669 5967.168,-217.1028 5971.3315,-220.3782 5972.4653,-217.0669"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->strings --> +<g id="edge36" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->strings</title> +<path fill="none" stroke="#000000" d="M6416.0841,-281.9306C6544.5215,-229.2862 6916.3574,-76.8768 7027.9606,-31.1324"/> +<polygon fill="#000000" stroke="#000000" points="7028.801,-32.6793 7032.7638,-29.1637 7027.4736,-29.4408 7028.801,-32.6793"/> +</g> +<!-- github.com/docker/distribution/registry/api/errcode->sync --> +<g id="edge37" class="edge"> +<title>github.com/docker/distribution/registry/api/errcode->sync</title> +<path fill="none" stroke="#000000" d="M6519.5478,-296.2471C6947.2792,-285.0714 8162.7384,-251.1032 8244,-224 8274.3541,-213.876 8444.6474,-90.2251 8513.8046,-39.4997"/> +<polygon fill="#000000" stroke="#000000" points="8515.2154,-40.635 8518.211,-36.2659 8513.1446,-37.8133 8515.2154,-40.635"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2 --> +<g id="node37" class="node"> +<title>github.com/docker/distribution/registry/api/v2</title> +<g id="a_node37"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/api/v2" xlink:title="github.com/docker/distribution/registry/api/v2" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7425,-412C7425,-412 7181,-412 7181,-412 7175,-412 7169,-406 7169,-400 7169,-400 7169,-388 7169,-388 7169,-382 7175,-376 7181,-376 7181,-376 7425,-376 7425,-376 7431,-376 7437,-382 7437,-388 7437,-388 7437,-400 7437,-400 7437,-406 7431,-412 7425,-412"/> +<text text-anchor="middle" x="7303" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/api/v2</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->fmt --> +<g id="edge38" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->fmt</title> +<path fill="none" stroke="#000000" d="M7225.8536,-375.9462C7178.2851,-363.2081 7116.8636,-343.8086 7066,-318 7028.2642,-298.8526 6797.381,-109.4839 6758,-94 6647.3291,-50.4862 6274.4881,-26.2055 6162.2402,-19.7572"/> +<polygon fill="#000000" stroke="#000000" points="6162.1857,-18.0013 6157.0941,-19.4636 6161.9863,-21.4957 6162.1857,-18.0013"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/reference --> +<g id="edge39" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M7334.6208,-375.8759C7362.1868,-360.0759 7402.1544,-337.1676 7430.9536,-320.6607"/> +<polygon fill="#000000" stroke="#000000" points="7432.0085,-322.0732 7435.4762,-318.0685 7430.268,-319.0367 7432.0085,-322.0732"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/opencontainers/go-digest --> +<g id="edge42" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M7437.1439,-384.2844C7564.654,-373.1916 7745.8999,-352.0404 7808,-318 7856.6152,-291.3514 7870.5395,-275.9121 7890,-224 7912.1163,-165.0035 7840.1859,-137.0402 7770.3892,-123.8107"/> +<polygon fill="#000000" stroke="#000000" points="7770.6036,-122.0708 7765.3689,-122.8835 7769.9678,-125.5126 7770.6036,-122.0708"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->net/http --> +<g id="edge43" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->net/http</title> +<path fill="none" stroke="#000000" d="M7168.9769,-391.9172C6917.8674,-386.8235 6386.0181,-370.0675 6210,-318 6178.3327,-308.6326 6175.0335,-295.7315 6145,-282 6128.7776,-274.583 6026.461,-237.6838 5972.0063,-218.1612"/> +<polygon fill="#000000" stroke="#000000" points="5972.5075,-216.4819 5967.2102,-216.4423 5971.3267,-219.7767 5972.5075,-216.4819"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->strings --> +<g id="edge46" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->strings</title> +<path fill="none" stroke="#000000" d="M7305.7882,-375.6878C7312.4671,-324.7617 7324.0582,-179.5889 7255,-94 7214.5487,-43.8657 7136.5994,-26.7209 7092.4921,-20.9175"/> +<polygon fill="#000000" stroke="#000000" points="7092.4647,-19.1504 7087.2858,-20.2645 7092.0291,-22.6232 7092.4647,-19.1504"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->regexp --> +<g id="edge45" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->regexp</title> +<path fill="none" stroke="#000000" d="M7437.1363,-386.2412C7550.0977,-377.1733 7715.0234,-357.9573 7854,-318 7891.4717,-307.2265 7897.7844,-296.4438 7934,-282 8006.5911,-253.0486 8054.7613,-287.7269 8100,-224 8113.5275,-204.9441 8132.8202,-137.3469 8099,-94 8067.2503,-53.3069 7910.6324,-30.0501 7841.6834,-21.6373"/> +<polygon fill="#000000" stroke="#000000" points="7841.6504,-19.8707 7836.4769,-21.01 7841.2316,-23.3456 7841.6504,-19.8707"/> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/registry/api/errcode --> +<g id="edge40" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M7168.6189,-380.432C6997.8745,-363.1925 6703.8999,-333.5108 6524.8004,-315.4278"/> +<polygon fill="#000000" stroke="#000000" points="6524.95,-313.684 6519.7994,-314.9228 6524.5983,-317.1663 6524.95,-313.684"/> +</g> +<!-- github.com/gorilla/mux --> +<g id="node38" class="node"> +<title>github.com/gorilla/mux</title> +<g id="a_node38"><a xlink:href="https://godoc.org/github.com/gorilla/mux" xlink:title="github.com/gorilla/mux" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7214,-318C7214,-318 7092,-318 7092,-318 7086,-318 7080,-312 7080,-306 7080,-306 7080,-294 7080,-294 7080,-288 7086,-282 7092,-282 7092,-282 7214,-282 7214,-282 7220,-282 7226,-288 7226,-294 7226,-294 7226,-306 7226,-306 7226,-312 7220,-318 7214,-318"/> +<text text-anchor="middle" x="7153" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/gorilla/mux</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->github.com/gorilla/mux --> +<g id="edge41" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->github.com/gorilla/mux</title> +<path fill="none" stroke="#000000" d="M7274.0785,-375.8759C7248.9695,-360.1409 7212.6107,-337.356 7186.295,-320.8648"/> +<polygon fill="#000000" stroke="#000000" points="7186.9988,-319.2407 7181.8327,-318.0685 7185.1403,-322.2065 7186.9988,-319.2407"/> +</g> +<!-- net/url --> +<g id="node39" class="node"> +<title>net/url</title> +<g id="a_node39"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M8688,-224C8688,-224 8658,-224 8658,-224 8652,-224 8646,-218 8646,-212 8646,-212 8646,-200 8646,-200 8646,-194 8652,-188 8658,-188 8658,-188 8688,-188 8688,-188 8694,-188 8700,-194 8700,-200 8700,-200 8700,-212 8700,-212 8700,-218 8694,-224 8688,-224"/> +<text text-anchor="middle" x="8673" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->net/url --> +<g id="edge44" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->net/url</title> +<path fill="none" stroke="#000000" d="M7437.1819,-390.1824C7734.4219,-381.0804 8430.0266,-355.9893 8530,-318 8580.5735,-298.7824 8627.5862,-255.021 8653.0971,-228.2438"/> +<polygon fill="#000000" stroke="#000000" points="8654.545,-229.2592 8656.7029,-224.4212 8651.9989,-226.8576 8654.545,-229.2592"/> +</g> +<!-- unicode --> +<g id="node40" class="node"> +<title>unicode</title> +<g id="a_node40"><a xlink:href="https://godoc.org/unicode" xlink:title="unicode" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M7781.5,-318C7781.5,-318 7746.5,-318 7746.5,-318 7740.5,-318 7734.5,-312 7734.5,-306 7734.5,-306 7734.5,-294 7734.5,-294 7734.5,-288 7740.5,-282 7746.5,-282 7746.5,-282 7781.5,-282 7781.5,-282 7787.5,-282 7793.5,-288 7793.5,-294 7793.5,-294 7793.5,-306 7793.5,-306 7793.5,-312 7787.5,-318 7781.5,-318"/> +<text text-anchor="middle" x="7764" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/api/v2->unicode --> +<g id="edge47" class="edge"> +<title>github.com/docker/distribution/registry/api/v2->unicode</title> +<path fill="none" stroke="#000000" d="M7426.661,-375.963C7508.3498,-362.8409 7617.3897,-343.0149 7712,-318 7717.7003,-316.4928 7723.6745,-314.6662 7729.4592,-312.7617"/> +<polygon fill="#000000" stroke="#000000" points="7730.0805,-314.3991 7734.2636,-311.1489 7728.9666,-311.0811 7730.0805,-314.3991"/> +</g> +<!-- github.com/gorilla/mux->bytes --> +<g id="edge126" class="edge"> +<title>github.com/gorilla/mux->bytes</title> +<path fill="none" stroke="#000000" d="M7079.6381,-297.532C6963.7556,-293.709 6731.2343,-286.3443 6534,-282 4812.8911,-244.0905 4381.5502,-281.9636 2661,-224 2636.0772,-223.1604 889.7949,-142.1176 868,-130 832.907,-110.4889 810.0965,-67.7352 798.6842,-41.0574"/> +<polygon fill="#000000" stroke="#000000" points="800.2275,-40.2116 796.6855,-36.2724 796.9979,-41.5606 800.2275,-40.2116"/> +</g> +<!-- github.com/gorilla/mux->context --> +<g id="edge127" class="edge"> +<title>github.com/gorilla/mux->context</title> +<path fill="none" stroke="#000000" d="M7226.0685,-292.0247C7258.7543,-288.6296 7297.7805,-284.8128 7333,-282 7763.5103,-247.6174 7879.9557,-310.9183 8303,-224 8308.5353,-222.8627 8314.284,-221.2393 8319.8273,-219.4274"/> +<polygon fill="#000000" stroke="#000000" points="8320.6784,-220.9863 8324.8482,-217.7189 8319.551,-217.6729 8320.6784,-220.9863"/> +</g> +<!-- github.com/gorilla/mux->errors --> +<g id="edge128" class="edge"> +<title>github.com/gorilla/mux->errors</title> +<path fill="none" stroke="#000000" d="M7079.6041,-284.5348C6964.7654,-260.8082 6735.4019,-215.3113 6539,-188 5733.2691,-75.9565 4736.2621,-27.8699 4545.3968,-19.3923"/> +<polygon fill="#000000" stroke="#000000" points="4545.3202,-17.6373 4540.2477,-19.1645 4545.1655,-21.1339 4545.3202,-17.6373"/> +</g> +<!-- github.com/gorilla/mux->fmt --> +<g id="edge129" class="edge"> +<title>github.com/gorilla/mux->fmt</title> +<path fill="none" stroke="#000000" d="M7132.8775,-281.8135C7106.2627,-258.3179 7057.302,-216.9978 7011,-188 6929.7994,-137.146 6909.0609,-120.5412 6817,-94 6691.1733,-57.7241 6280.5444,-28.0617 6162.1934,-20.1035"/> +<polygon fill="#000000" stroke="#000000" points="6162.2166,-18.3512 6157.1108,-19.763 6161.9826,-21.8433 6162.2166,-18.3512"/> +</g> +<!-- github.com/gorilla/mux->net/http --> +<g id="edge130" class="edge"> +<title>github.com/gorilla/mux->net/http</title> +<path fill="none" stroke="#000000" d="M7079.8346,-295.695C6908.4256,-285.3962 6467.5572,-257.6714 6100,-224 6055.864,-219.9568 6005.0962,-214.1234 5972.3315,-210.2038"/> +<polygon fill="#000000" stroke="#000000" points="5972.4042,-208.45 5967.2313,-209.5917 5971.9871,-211.9251 5972.4042,-208.45"/> +</g> +<!-- github.com/gorilla/mux->strings --> +<g id="edge135" class="edge"> +<title>github.com/gorilla/mux->strings</title> +<path fill="none" stroke="#000000" d="M7143.8457,-281.6846C7133.6161,-258.415 7120.513,-217.7127 7137,-188 7160.635,-145.4051 7206.365,-172.5949 7230,-130 7237.7631,-116.0095 7238.704,-107.4254 7230,-94 7199.5355,-47.0102 7132.463,-28.8147 7092.2726,-21.9607"/> +<polygon fill="#000000" stroke="#000000" points="7092.2442,-20.183 7087.0275,-21.1045 7091.6803,-23.6373 7092.2442,-20.183"/> +</g> +<!-- github.com/gorilla/mux->path --> +<g id="edge132" class="edge"> +<title>github.com/gorilla/mux->path</title> +<path fill="none" stroke="#000000" d="M7157.8203,-281.8759C7161.8322,-266.7911 7167.5673,-245.227 7171.8988,-228.9405"/> +<polygon fill="#000000" stroke="#000000" points="7173.6006,-229.3504 7173.1945,-224.0685 7170.2181,-228.4507 7173.6006,-229.3504"/> +</g> +<!-- github.com/gorilla/mux->regexp --> +<g id="edge133" class="edge"> +<title>github.com/gorilla/mux->regexp</title> +<path fill="none" stroke="#000000" d="M7226.1187,-292.5912C7258.8139,-289.3034 7297.8337,-285.4138 7333,-282 7401.4255,-275.3575 7902.7229,-274.839 7949,-224 8005.1702,-162.2926 7897.1084,-76.5632 7840.4939,-38.0903"/> +<polygon fill="#000000" stroke="#000000" points="7841.3019,-36.5245 7836.1777,-35.1809 7839.3455,-39.4268 7841.3019,-36.5245"/> +</g> +<!-- github.com/gorilla/mux->net/url --> +<g id="edge131" class="edge"> +<title>github.com/gorilla/mux->net/url</title> +<path fill="none" stroke="#000000" d="M7226.0617,-291.9381C7258.7462,-288.5266 7297.7733,-284.721 7333,-282 7804.742,-245.5619 7923.6957,-252.2299 8396,-224 8484.5989,-218.7044 8589.0865,-211.7119 8640.7438,-208.2051"/> +<polygon fill="#000000" stroke="#000000" points="8641.0065,-209.9414 8645.8764,-207.8563 8640.7692,-206.4494 8641.0065,-209.9414"/> +</g> +<!-- strconv --> +<g id="node47" class="node"> +<title>strconv</title> +<g id="a_node47"><a xlink:href="https://godoc.org/strconv" xlink:title="strconv" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3806,-36C3806,-36 3774,-36 3774,-36 3768,-36 3762,-30 3762,-24 3762,-24 3762,-12 3762,-12 3762,-6 3768,0 3774,0 3774,0 3806,0 3806,0 3812,0 3818,-6 3818,-12 3818,-12 3818,-24 3818,-24 3818,-30 3812,-36 3806,-36"/> +<text text-anchor="middle" x="3790" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strconv</text> +</a> +</g> +</g> +<!-- github.com/gorilla/mux->strconv --> +<g id="edge134" class="edge"> +<title>github.com/gorilla/mux->strconv</title> +<path fill="none" stroke="#000000" d="M7079.529,-295.6976C6963.0673,-287.8192 6729.4433,-267.6956 6536,-224 6486.647,-212.852 6477.6328,-197.8278 6428,-188 6279.8482,-158.6646 5222.734,-103.4232 5072,-94 4609.5186,-65.0876 4493.4025,-66.147 4031,-36 3956.8218,-31.1638 3869.8856,-24.4002 3823.3797,-20.6934"/> +<polygon fill="#000000" stroke="#000000" points="3823.3094,-18.9323 3818.186,-20.2787 3823.0308,-22.4212 3823.3094,-18.9323"/> +</g> +<!-- github.com/docker/distribution/registry/client --> +<g id="node41" class="node"> +<title>github.com/docker/distribution/registry/client</title> +<g id="a_node41"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client" xlink:title="github.com/docker/distribution/registry/client" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6492,-976C6492,-976 6252,-976 6252,-976 6246,-976 6240,-970 6240,-964 6240,-964 6240,-952 6240,-952 6240,-946 6246,-940 6252,-940 6252,-940 6492,-940 6492,-940 6498,-940 6504,-946 6504,-952 6504,-952 6504,-964 6504,-964 6504,-970 6498,-976 6492,-976"/> +<text text-anchor="middle" x="6372" y="-954.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->bytes --> +<g id="edge48" class="edge"> +<title>github.com/docker/distribution/registry/client->bytes</title> +<path fill="none" stroke="#000000" d="M6239.8841,-956.3358C5854.1248,-950.9445 4733.7722,-931.3616 4371,-882 4246.771,-865.0965 3944.6315,-778.0785 3822,-752 3381.3697,-658.2968 3269.0093,-645.7194 2826,-564 1987.9671,-409.4128 1695.1914,-616.9007 939,-224 862.6637,-184.3373 816.1983,-85.6669 798.3665,-40.8254"/> +<polygon fill="#000000" stroke="#000000" points="799.9854,-40.16 796.5312,-36.1436 796.7268,-41.4374 799.9854,-40.16"/> +</g> +<!-- github.com/docker/distribution/registry/client->context --> +<g id="edge49" class="edge"> +<title>github.com/docker/distribution/registry/client->context</title> +<path fill="none" stroke="#000000" d="M6504.3136,-957.1699C6878.0813,-954.112 7924.0437,-940.3796 8067,-882 8136.0575,-853.7987 8192,-844.5939 8192,-770 8192,-770 8192,-770 8192,-582 8192,-442.923 8020.0072,-393.6002 8103,-282 8156.4484,-210.128 8210.4041,-250.3753 8296,-224 8303.8022,-221.5959 8312.1921,-218.9694 8320.0716,-216.4844"/> +<polygon fill="#000000" stroke="#000000" points="8320.6613,-218.1334 8324.902,-214.9586 8319.6071,-214.796 8320.6613,-218.1334"/> +</g> +<!-- github.com/docker/distribution/registry/client->encoding/json --> +<g id="edge50" class="edge"> +<title>github.com/docker/distribution/registry/client->encoding/json</title> +<path fill="none" stroke="#000000" d="M6239.7777,-956.6625C5798.2479,-951.7432 4391.4245,-932.256 4194,-882 4159.3286,-873.1741 4156.1632,-856.6242 4122,-846 3992.1491,-805.6187 1831.743,-530.5908 1698,-506 1124.0701,-400.4737 942.8355,-426.1212 440,-130 398.3743,-105.4865 358.8378,-64.9081 336.5774,-39.91"/> +<polygon fill="#000000" stroke="#000000" points="337.7774,-38.6251 333.1554,-36.0366 335.1544,-40.9424 337.7774,-38.6251"/> +</g> +<!-- github.com/docker/distribution/registry/client->errors --> +<g id="edge51" class="edge"> +<title>github.com/docker/distribution/registry/client->errors</title> +<path fill="none" stroke="#000000" d="M6239.8535,-955.7614C5831.7025,-948.4218 4614.0277,-923.1693 4540,-882 4399.0858,-803.6328 4336,-743.2396 4336,-582 4336,-582 4336,-582 4336,-300 4336,-240.8478 4449.134,-96.0307 4494.5043,-40.3328"/> +<polygon fill="#000000" stroke="#000000" points="4496.1015,-41.1438 4497.9091,-36.1642 4493.3908,-38.9297 4496.1015,-41.1438"/> +</g> +<!-- github.com/docker/distribution/registry/client->fmt --> +<g id="edge52" class="edge"> +<title>github.com/docker/distribution/registry/client->fmt</title> +<path fill="none" stroke="#000000" d="M6239.718,-940.1565C6099.7727,-913.4463 5885.6975,-848.452 5786,-694 5724.2614,-598.3541 5796.7871,-278.3846 5866,-188 5896.3072,-148.4221 5919.9177,-156.7212 5962,-130 6010.5701,-99.1592 6065.9174,-61.8265 6099.3867,-39.0035"/> +<polygon fill="#000000" stroke="#000000" points="6100.6165,-40.283 6103.7599,-36.0191 6098.6435,-37.3921 6100.6165,-40.283"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution --> +<g id="edge53" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M6384.7227,-939.756C6406.1206,-907.2353 6447,-836.4151 6447,-770 6447,-770 6447,-770 6447,-582 6447,-505.0762 6526.0576,-444.7889 6575.0561,-414.8111"/> +<polygon fill="#000000" stroke="#000000" points="6576.0343,-416.2647 6579.4074,-412.18 6574.2233,-413.2697 6576.0343,-416.2647"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/reference --> +<g id="edge54" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M6504.0124,-951.1909C6818.1362,-933.2294 7584,-878.3297 7584,-770 7584,-770 7584,-770 7584,-488 7584,-418.8237 7526.1934,-354.0101 7491.8451,-321.6043"/> +<polygon fill="#000000" stroke="#000000" points="7493.0377,-320.3235 7488.1878,-318.1924 7490.6501,-322.8828 7493.0377,-320.3235"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/opencontainers/go-digest --> +<g id="edge61" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M6504.1012,-955.4191C6923.204,-946.8979 8206.2239,-918.1341 8390,-882 8509.3379,-858.5358 8558.0615,-870.9581 8647,-788 8847.3161,-601.1534 9024.1967,-326.742 8788,-188 8701.8273,-137.3822 8040.813,-119.2558 7770.5715,-113.9172"/> +<polygon fill="#000000" stroke="#000000" points="7770.474,-112.165 7765.4406,-113.8164 7770.4052,-115.6643 7770.474,-112.165"/> +</g> +<!-- github.com/docker/distribution/registry/client->io --> +<g id="edge62" class="edge"> +<title>github.com/docker/distribution/registry/client->io</title> +<path fill="none" stroke="#000000" d="M6239.7013,-948.7476C5866.4357,-921.7716 4832,-840.6402 4832,-770 4832,-770 4832,-770 4832,-488 4832,-315.9977 4923.9692,-292.9614 4979,-130 4989.2387,-99.6803 4998.3302,-63.8257 5003.7509,-41.0032"/> +<polygon fill="#000000" stroke="#000000" points="5005.4729,-41.3247 5004.9167,-36.0566 5002.0663,-40.5218 5005.4729,-41.3247"/> +</g> +<!-- github.com/docker/distribution/registry/client->net/http --> +<g id="edge64" class="edge"> +<title>github.com/docker/distribution/registry/client->net/http</title> +<path fill="none" stroke="#000000" d="M6278.8422,-939.9224C6191.7419,-916.9728 6075,-867.9443 6075,-770 6075,-770 6075,-770 6075,-676 6075,-499.4845 6107.9956,-437.7866 6025,-282 6012.7263,-258.9618 5990.4441,-239.7043 5971.5032,-226.3385"/> +<polygon fill="#000000" stroke="#000000" points="5972.3761,-224.8144 5967.2688,-223.4079 5970.3843,-227.6923 5972.3761,-224.8144"/> +</g> +<!-- github.com/docker/distribution/registry/client->strings --> +<g id="edge67" class="edge"> +<title>github.com/docker/distribution/registry/client->strings</title> +<path fill="none" stroke="#000000" d="M6473.7946,-939.9905C6616.5597,-911.7897 6859,-851.8501 6859,-770 6859,-770 6859,-770 6859,-676 6859,-416.5922 7003.6542,-123.7693 7047.5321,-40.9145"/> +<polygon fill="#000000" stroke="#000000" points="7049.2289,-41.451 7050.0324,-36.2149 7046.1389,-39.8071 7049.2289,-41.451"/> +</g> +<!-- github.com/docker/distribution/registry/client->time --> +<g id="edge68" class="edge"> +<title>github.com/docker/distribution/registry/client->time</title> +<path fill="none" stroke="#000000" d="M6239.7523,-952.6864C5875.4117,-937.8978 4879.4085,-896.4037 4810,-882 4671.8691,-853.3351 4513,-911.0738 4513,-770 4513,-770 4513,-770 4513,-582 4513,-347.5489 4807.5411,-177.7259 4906.2189,-127.4101"/> +<polygon fill="#000000" stroke="#000000" points="4907.099,-128.9261 4910.7678,-125.1047 4905.5167,-125.8041 4907.099,-128.9261"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/errcode --> +<g id="edge55" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/errcode</title> +<path fill="none" stroke="#000000" d="M6372,-939.8515C6372,-906.3085 6372,-832.3403 6372,-770 6372,-770 6372,-770 6372,-488 6372,-428.8254 6372,-359.1739 6372,-323.5645"/> +<polygon fill="#000000" stroke="#000000" points="6373.7501,-323.1485 6372,-318.1485 6370.2501,-323.1486 6373.7501,-323.1485"/> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/v2 --> +<g id="edge56" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/api/v2</title> +<path fill="none" stroke="#000000" d="M6504.2267,-948.694C6632.9091,-937.7931 6817.984,-916.6723 6881,-882 7093.638,-765.0034 7248.6461,-496.4469 7291.2428,-416.7168"/> +<polygon fill="#000000" stroke="#000000" points="7292.853,-417.416 7293.6542,-412.1795 7289.7624,-415.7734 7292.853,-417.416"/> +</g> +<!-- github.com/docker/distribution/registry/client->net/url --> +<g id="edge65" class="edge"> +<title>github.com/docker/distribution/registry/client->net/url</title> +<path fill="none" stroke="#000000" d="M6504.1536,-955.0086C6856.9815,-946.5867 7815.6192,-920.738 8130,-882 8354.2983,-854.362 8626,-995.9946 8626,-770 8626,-770 8626,-770 8626,-394 8626,-332.9136 8649.6433,-263.9063 8663.3914,-228.9434"/> +<polygon fill="#000000" stroke="#000000" points="8665.1211,-229.3297 8665.344,-224.037 8661.8692,-228.0355 8665.1211,-229.3297"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge --> +<g id="node42" class="node"> +<title>github.com/docker/distribution/registry/client/auth/challenge</title> +<g id="a_node42"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" xlink:title="github.com/docker/distribution/registry/client/auth/challenge" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M8452.5,-318C8452.5,-318 8129.5,-318 8129.5,-318 8123.5,-318 8117.5,-312 8117.5,-306 8117.5,-306 8117.5,-294 8117.5,-294 8117.5,-288 8123.5,-282 8129.5,-282 8129.5,-282 8452.5,-282 8452.5,-282 8458.5,-282 8464.5,-288 8464.5,-294 8464.5,-294 8464.5,-306 8464.5,-306 8464.5,-312 8458.5,-318 8452.5,-318"/> +<text text-anchor="middle" x="8291" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth/challenge</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/auth/challenge --> +<g id="edge57" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/auth/challenge</title> +<path fill="none" stroke="#000000" d="M6504.2726,-956.7173C6867.6837,-952.5076 7863.5619,-936.1179 8001,-882 8072.589,-853.811 8133,-846.939 8133,-770 8133,-770 8133,-770 8133,-582 8133,-472.5579 8224.6906,-366.2088 8268.3743,-321.7517"/> +<polygon fill="#000000" stroke="#000000" points="8269.6661,-322.9342 8271.9415,-318.1503 8267.1795,-320.4711 8269.6661,-322.9342"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport --> +<g id="node43" class="node"> +<title>github.com/docker/distribution/registry/client/transport</title> +<g id="a_node43"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/transport" xlink:title="github.com/docker/distribution/registry/client/transport" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M5711,-318C5711,-318 5419,-318 5419,-318 5413,-318 5407,-312 5407,-306 5407,-306 5407,-294 5407,-294 5407,-288 5413,-282 5419,-282 5419,-282 5711,-282 5711,-282 5717,-282 5723,-288 5723,-294 5723,-294 5723,-306 5723,-306 5723,-312 5717,-318 5711,-318"/> +<text text-anchor="middle" x="5565" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/transport</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/transport --> +<g id="edge58" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/client/transport</title> +<path fill="none" stroke="#000000" d="M6239.9089,-950.9787C6063.6676,-940.3024 5769.6297,-917.4076 5735,-882 5577.0197,-720.4713 5804.7226,-568.2348 5686,-376 5670.4155,-350.7657 5642.996,-332.5249 5618.1953,-320.2493"/> +<polygon fill="#000000" stroke="#000000" points="5618.8615,-318.6277 5613.5983,-318.0263 5617.3377,-321.7786 5618.8615,-318.6277"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache --> +<g id="node44" class="node"> +<title>github.com/docker/distribution/registry/storage/cache</title> +<g id="a_node44"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache" xlink:title="github.com/docker/distribution/registry/storage/cache" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6803,-788C6803,-788 6519,-788 6519,-788 6513,-788 6507,-782 6507,-776 6507,-776 6507,-764 6507,-764 6507,-758 6513,-752 6519,-752 6519,-752 6803,-752 6803,-752 6809,-752 6815,-758 6815,-764 6815,-764 6815,-776 6815,-776 6815,-782 6809,-788 6803,-788"/> +<text text-anchor="middle" x="6661" y="-766.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache --> +<g id="edge59" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache</title> +<path fill="none" stroke="#000000" d="M6399.7105,-939.9738C6453.41,-905.0412 6571.3821,-828.2981 6628.6025,-791.0752"/> +<polygon fill="#000000" stroke="#000000" points="6629.7118,-792.4413 6632.9487,-788.2479 6627.8032,-789.5075 6629.7118,-792.4413"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory --> +<g id="node45" class="node"> +<title>github.com/docker/distribution/registry/storage/cache/memory</title> +<g id="a_node45"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" xlink:title="github.com/docker/distribution/registry/storage/cache/memory" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M7974,-882C7974,-882 7640,-882 7640,-882 7634,-882 7628,-876 7628,-870 7628,-870 7628,-858 7628,-858 7628,-852 7634,-846 7640,-846 7640,-846 7974,-846 7974,-846 7980,-846 7986,-852 7986,-858 7986,-858 7986,-870 7986,-870 7986,-876 7980,-882 7974,-882"/> +<text text-anchor="middle" x="7807" y="-860.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/storage/cache/memory</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache/memory --> +<g id="edge60" class="edge"> +<title>github.com/docker/distribution/registry/client->github.com/docker/distribution/registry/storage/cache/memory</title> +<path fill="none" stroke="#000000" d="M6504.0265,-951.0763C6729.9011,-938.9606 7204.9576,-912.301 7606,-882 7611.4833,-881.5857 7617.0498,-881.1546 7622.6686,-880.7102"/> +<polygon fill="#000000" stroke="#000000" points="7622.9972,-882.4396 7627.8426,-880.2984 7622.7195,-878.9507 7622.9972,-882.4396"/> +</g> +<!-- io/ioutil --> +<g id="node46" class="node"> +<title>io/ioutil</title> +<g id="a_node46"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M194.5,-36C194.5,-36 159.5,-36 159.5,-36 153.5,-36 147.5,-30 147.5,-24 147.5,-24 147.5,-12 147.5,-12 147.5,-6 153.5,0 159.5,0 159.5,0 194.5,0 194.5,0 200.5,0 206.5,-6 206.5,-12 206.5,-12 206.5,-24 206.5,-24 206.5,-30 200.5,-36 194.5,-36"/> +<text text-anchor="middle" x="177" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client->io/ioutil --> +<g id="edge63" class="edge"> +<title>github.com/docker/distribution/registry/client->io/ioutil</title> +<path fill="none" stroke="#000000" d="M6239.6462,-955.3823C5821.9617,-946.83 4534.8487,-918.17 4119,-882 2942.2289,-779.646 2652.824,-702.0907 1488,-506 907.5054,-408.2775 633.3783,-547.1145 218,-130 193.9922,-105.8919 183.9124,-66.591 179.7718,-41.5318"/> +<polygon fill="#000000" stroke="#000000" points="181.4731,-41.0829 178.9755,-36.4113 178.0146,-41.6208 181.4731,-41.0829"/> +</g> +<!-- github.com/docker/distribution/registry/client->strconv --> +<g id="edge66" class="edge"> +<title>github.com/docker/distribution/registry/client->strconv</title> +<path fill="none" stroke="#000000" d="M6239.7899,-955.8941C5837.082,-949.0354 4642.7076,-925.2498 4473,-882 4346.6496,-849.7997 4330.4703,-801.8819 4210,-752 4089.4941,-702.1034 4057.9072,-692.0403 3932,-658 3610.2109,-571.0009 2662.4906,-590.8524 2471,-318 2409.8306,-230.8405 2464.2397,-148.0232 2556,-94 2662.1706,-31.4928 3570.5083,-19.9746 3756.724,-18.2638"/> +<polygon fill="#000000" stroke="#000000" points="3756.7801,-20.0134 3761.7641,-18.2183 3756.7485,-16.5136 3756.7801,-20.0134"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->fmt --> +<g id="edge80" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->fmt</title> +<path fill="none" stroke="#000000" d="M8328.6997,-281.9734C8370.1564,-259.8527 8427.0374,-221.1934 8396,-188 8330.3613,-117.8019 7628.8466,-137.0462 7533,-130 6984.4918,-89.6763 6315.9404,-33.68 6162.4501,-20.742"/> +<polygon fill="#000000" stroke="#000000" points="6162.3377,-18.9764 6157.2083,-20.3 6162.0436,-22.464 6162.3377,-18.9764"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->net/http --> +<g id="edge81" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->net/http</title> +<path fill="none" stroke="#000000" d="M8117.4714,-293.3943C8025.5029,-289.9284 7910.6227,-285.6546 7808,-282 7048.9325,-254.9678 6857.5643,-278.8683 6100,-224 6055.795,-220.7984 6005.0427,-214.7768 5972.3003,-210.5842"/> +<polygon fill="#000000" stroke="#000000" points="5972.3863,-208.8309 5967.2037,-209.9278 5971.9391,-212.3023 5972.3863,-208.8309"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->strings --> +<g id="edge83" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->strings</title> +<path fill="none" stroke="#000000" d="M8354.05,-281.9158C8411.4297,-262.3707 8482.2084,-228.5182 8447,-188 8354.7791,-81.8711 7292.2228,-28.5492 7092.3176,-19.423"/> +<polygon fill="#000000" stroke="#000000" points="7092.279,-17.6695 7087.2046,-19.1907 7092.12,-21.1659 7092.279,-17.6695"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->sync --> +<g id="edge84" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->sync</title> +<path fill="none" stroke="#000000" d="M8366.4468,-281.9368C8420.6772,-267.4232 8487.3815,-245.9097 8506,-224 8550.7905,-171.2917 8549.4278,-83.0679 8545.7841,-41.2899"/> +<polygon fill="#000000" stroke="#000000" points="8547.5153,-41.0061 8545.3075,-36.1907 8544.0305,-41.3319 8547.5153,-41.0061"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth/challenge->net/url --> +<g id="edge82" class="edge"> +<title>github.com/docker/distribution/registry/client/auth/challenge->net/url</title> +<path fill="none" stroke="#000000" d="M8364.2015,-281.9871C8447.6454,-261.4537 8580.3024,-228.8104 8640.9035,-213.8981"/> +<polygon fill="#000000" stroke="#000000" points="8641.4039,-215.5772 8645.8409,-212.6831 8640.5675,-212.1786 8641.4039,-215.5772"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->errors --> +<g id="edge85" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->errors</title> +<path fill="none" stroke="#000000" d="M5546.4034,-281.9175C5502.2626,-240.2626 5385.9328,-137.7716 5267,-94 5131.0614,-43.9697 4671.1276,-23.7774 4545.1406,-19.108"/> +<polygon fill="#000000" stroke="#000000" points="4545.1567,-17.3575 4540.0958,-18.9228 4545.0282,-20.8552 4545.1567,-17.3575"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->fmt --> +<g id="edge86" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->fmt</title> +<path fill="none" stroke="#000000" d="M5599.8075,-281.9812C5642.9358,-259.7053 5718.7196,-220.7271 5784,-188 5899.1652,-130.264 6036.7561,-63.2418 6097.9408,-33.5389"/> +<polygon fill="#000000" stroke="#000000" points="6098.8315,-35.0519 6102.5655,-31.2942 6097.3031,-31.9032 6098.8315,-35.0519"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->io --> +<g id="edge87" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->io</title> +<path fill="none" stroke="#000000" d="M5569.7605,-281.7436C5574.8055,-258.1725 5579.8257,-216.7708 5562,-188 5515.0876,-112.2831 5468.859,-124.0248 5385,-94 5262.1725,-50.023 5107.1537,-28.8474 5041.1337,-21.3284"/> +<polygon fill="#000000" stroke="#000000" points="5041.3216,-19.5885 5036.1572,-20.7683 5040.9301,-23.0666 5041.3216,-19.5885"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->net/http --> +<g id="edge88" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->net/http</title> +<path fill="none" stroke="#000000" d="M5636.4768,-281.9871C5716.7203,-261.7649 5843.5722,-229.7968 5903.922,-214.588"/> +<polygon fill="#000000" stroke="#000000" points="5904.4308,-216.2646 5908.8515,-213.3457 5903.5754,-212.8707 5904.4308,-216.2646"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->sync --> +<g id="edge91" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->sync</title> +<path fill="none" stroke="#000000" d="M5723.1903,-295.3272C6203.886,-281.0077 7620.6665,-237.8697 7719,-224 8033.0535,-179.7034 8401.0472,-64.4253 8510.6514,-28.7091"/> +<polygon fill="#000000" stroke="#000000" points="8511.4643,-30.2847 8515.6741,-27.0691 8510.3779,-26.9576 8511.4643,-30.2847"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->regexp --> +<g id="edge89" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->regexp</title> +<path fill="none" stroke="#000000" d="M5723.4072,-295.5813C6204.1798,-282.0003 7616.6639,-240.7779 7662,-224 7724.8827,-200.7285 7743.4936,-186.2413 7780,-130 7797.6904,-102.7464 7804.6033,-65.2148 7807.2956,-41.2767"/> +<polygon fill="#000000" stroke="#000000" points="7809.0585,-41.2436 7807.8368,-36.089 7805.5774,-40.8803 7809.0585,-41.2436"/> +</g> +<!-- github.com/docker/distribution/registry/client/transport->strconv --> +<g id="edge90" class="edge"> +<title>github.com/docker/distribution/registry/client/transport->strconv</title> +<path fill="none" stroke="#000000" d="M5525.9351,-281.9789C5433.3327,-240.2232 5192.2472,-137.1327 4979,-94 4565.2575,-10.3138 4452.1505,-64.6096 4031,-36 3956.8353,-30.9618 3869.8939,-24.2758 3823.3836,-20.6359"/> +<polygon fill="#000000" stroke="#000000" points="3823.3108,-18.8749 3818.1893,-20.2289 3823.0373,-22.3642 3823.3108,-18.8749"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->context --> +<g id="edge92" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->context</title> +<path fill="none" stroke="#000000" d="M6716.7375,-751.9582C6894.0245,-694.1415 7460.0546,-506.2278 7916,-318 7950.9192,-303.5843 7957.0604,-293.6407 7993,-282 8123.4401,-239.7508 8163.0666,-257.5904 8296,-224 8303.8321,-222.0209 8312.1714,-219.5811 8319.9854,-217.1437"/> +<polygon fill="#000000" stroke="#000000" points="8320.5333,-218.806 8324.7736,-215.6306 8319.4786,-215.4687 8320.5333,-218.806"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->fmt --> +<g id="edge93" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->fmt</title> +<path fill="none" stroke="#000000" d="M6676.2996,-751.879C6701.7461,-719.8261 6750,-650.0702 6750,-582 6750,-582 6750,-582 6750,-488 6750,-391.7401 6647.9792,-150.4378 6570,-94 6504.3493,-46.4849 6252.4305,-25.8879 6162.3012,-19.9345"/> +<polygon fill="#000000" stroke="#000000" points="6162.2136,-18.1751 6157.1103,-19.596 6161.9859,-21.6677 6162.2136,-18.1751"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution --> +<g id="edge94" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M6658.6849,-751.8647C6650.782,-689.9589 6624.8704,-486.9851 6616.0057,-417.5443"/> +<polygon fill="#000000" stroke="#000000" points="6617.7173,-417.1319 6615.3481,-412.3938 6614.2455,-417.5752 6617.7173,-417.1319"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/opencontainers/go-digest --> +<g id="edge96" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M6815.2696,-757.1066C7262.0172,-715.8381 8536.3819,-568.9679 8715,-224 8722.3568,-209.7917 8725.9013,-199.7116 8715,-188 8683.1785,-153.813 8037.7854,-126.1157 7770.4445,-116.0651"/> +<polygon fill="#000000" stroke="#000000" points="7770.4297,-114.3134 7765.3675,-115.8747 7770.2984,-117.811 7770.4297,-114.3134"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution/metrics --> +<g id="edge95" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache->github.com/docker/distribution/metrics</title> +<path fill="none" stroke="#000000" d="M6518.238,-751.9871C6378.391,-734.3419 6167.6671,-707.754 6036.1521,-691.1601"/> +<polygon fill="#000000" stroke="#000000" points="6036.2982,-689.4148 6031.1184,-690.525 6035.86,-692.8872 6036.2982,-689.4148"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->context --> +<g id="edge97" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->context</title> +<path fill="none" stroke="#000000" d="M7854.1984,-845.9537C7915.5671,-819.0462 8015,-762.1557 8015,-676 8015,-676 8015,-676 8015,-394 8015,-341.5763 8013.377,-317.4473 8052,-282 8134.1223,-206.6301 8188.5611,-253.6914 8296,-224 8303.8692,-221.8253 8312.285,-219.2873 8320.1681,-216.8146"/> +<polygon fill="#000000" stroke="#000000" points="8320.7584,-218.4635 8324.998,-215.2874 8319.7031,-215.1264 8320.7584,-218.4635"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution --> +<g id="edge98" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution</title> +<path fill="none" stroke="#000000" d="M7760.9254,-845.8634C7572.4308,-771.6654 6860.7045,-491.5051 6663.7736,-413.9862"/> +<polygon fill="#000000" stroke="#000000" points="6664.2308,-412.2856 6658.9373,-412.0825 6662.9488,-415.5424 6664.2308,-412.2856"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/reference --> +<g id="edge99" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/reference</title> +<path fill="none" stroke="#000000" d="M7802.662,-845.9752C7795.0297,-812.6333 7780,-738.9799 7780,-676 7780,-676 7780,-676 7780,-488 7780,-390.4814 7666.9653,-342.2761 7576.7509,-319.3323"/> +<polygon fill="#000000" stroke="#000000" points="7576.9349,-317.5745 7571.6596,-318.0579 7576.085,-320.9697 7576.9349,-317.5745"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/opencontainers/go-digest --> +<g id="edge101" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/opencontainers/go-digest</title> +<path fill="none" stroke="#000000" d="M7855.3582,-845.8824C7892.8835,-831.3808 7945.9274,-809.9085 7991,-788 8185.9651,-693.233 8902.9006,-484.296 8825,-282 8804.759,-229.4372 8790.6488,-212.6423 8740,-188 8654.4945,-146.3988 8031.3369,-123.1859 7770.4142,-115.2003"/> +<polygon fill="#000000" stroke="#000000" points="7770.1293,-113.4409 7765.0782,-115.0376 7770.0225,-116.9393 7770.1293,-113.4409"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->sync --> +<g id="edge102" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->sync</title> +<path fill="none" stroke="#000000" d="M7986.0494,-849.7626C8233.436,-818.823 8666.1215,-720.9409 8815,-412 8878.872,-279.458 8827.647,-197.4216 8723,-94 8681.5532,-53.0385 8614.6777,-32.8282 8575.1646,-23.964"/> +<polygon fill="#000000" stroke="#000000" points="8575.271,-22.1961 8570.0126,-22.8379 8574.5235,-25.6153 8575.271,-22.1961"/> +</g> +<!-- github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/registry/storage/cache --> +<g id="edge100" class="edge"> +<title>github.com/docker/distribution/registry/storage/cache/memory->github.com/docker/distribution/registry/storage/cache</title> +<path fill="none" stroke="#000000" d="M7627.9375,-849.3125C7406.4702,-831.1468 7032.8284,-800.499 6820.6984,-783.0992"/> +<polygon fill="#000000" stroke="#000000" points="6820.5322,-781.3297 6815.4058,-782.6651 6820.246,-784.818 6820.5322,-781.3297"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth --> +<g id="node48" class="node"> +<title>github.com/docker/distribution/registry/client/auth</title> +<g id="a_node48"><a xlink:href="https://godoc.org/github.com/docker/distribution/registry/client/auth" xlink:title="github.com/docker/distribution/registry/client/auth" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M6505.5,-1070C6505.5,-1070 6238.5,-1070 6238.5,-1070 6232.5,-1070 6226.5,-1064 6226.5,-1058 6226.5,-1058 6226.5,-1046 6226.5,-1046 6226.5,-1040 6232.5,-1034 6238.5,-1034 6238.5,-1034 6505.5,-1034 6505.5,-1034 6511.5,-1034 6517.5,-1040 6517.5,-1046 6517.5,-1046 6517.5,-1058 6517.5,-1058 6517.5,-1064 6511.5,-1070 6505.5,-1070"/> +<text text-anchor="middle" x="6372" y="-1048.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/distribution/registry/client/auth</text> +</a> +</g> +</g> +<!-- github.com/docker/distribution/registry/client/auth->encoding/json --> +<g id="edge69" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->encoding/json</title> +<path fill="none" stroke="#000000" d="M6226.1755,-1051.6206C5275.634,-1048.6835 0,-1025.1245 0,-864 0,-864 0,-864 0,-206 0,-144.7254 177.1668,-70.1826 267.0222,-36.3165"/> +<polygon fill="#000000" stroke="#000000" points="267.694,-37.9336 271.7601,-34.538 266.464,-34.6569 267.694,-37.9336"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->errors --> +<g id="edge70" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->errors</title> +<path fill="none" stroke="#000000" d="M6226.4501,-1049.7529C5807.3857,-1042.8268 4602.9937,-1019.5954 4211,-976 3943.9943,-946.3051 3617,-1132.6519 3617,-864 3617,-864 3617,-864 3617,-300 3617,-236.4196 3650.7543,-219.4703 3706,-188 3870.8837,-94.0753 3944.4458,-174.1389 4129,-130 4176.5715,-118.6226 4186.2591,-108.4148 4233,-94 4322.0611,-66.5337 4428.6537,-39.0685 4480.9088,-25.9514"/> +<polygon fill="#000000" stroke="#000000" points="4481.369,-27.6402 4485.7937,-24.7272 4480.5182,-24.2452 4481.369,-27.6402"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->fmt --> +<g id="edge71" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->fmt</title> +<path fill="none" stroke="#000000" d="M6517.6825,-1036.0464C6696.7013,-1012.3037 6977,-959.828 6977,-864 6977,-864 6977,-864 6977,-488 6977,-248.6903 6821.2694,-206.4028 6610,-94 6531.5052,-52.238 6256.9035,-27.7267 6162.3113,-20.3684"/> +<polygon fill="#000000" stroke="#000000" points="6162.2618,-18.6094 6157.1419,-19.9694 6161.9924,-22.0991 6162.2618,-18.6094"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->net/http --> +<g id="edge75" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->net/http</title> +<path fill="none" stroke="#000000" d="M6228.4133,-1033.9966C6059.5789,-1009.535 5794.7019,-959.7244 5735,-882 5692.3288,-826.4475 5627.2968,-819.4818 5786,-658 5850.8732,-591.991 5930.4124,-673.9983 5986,-600 6072.6954,-484.591 5985.3755,-294.2517 5950.7715,-228.8766"/> +<polygon fill="#000000" stroke="#000000" points="5952.2735,-227.9742 5948.3742,-224.3883 5949.1863,-229.6231 5952.2735,-227.9742"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->strings --> +<g id="edge77" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->strings</title> +<path fill="none" stroke="#000000" d="M6517.7587,-1049.4192C7074.9444,-1038.6094 9039,-991.4654 9039,-864 9039,-864 9039,-864 9039,-394 9039,-264.8348 8949.2914,-246.2369 8834,-188 8580.5237,-59.9621 8484.3531,-124.3424 8202,-94 7763.6098,-46.8894 7227.1976,-24.3483 7092.0602,-19.1787"/> +<polygon fill="#000000" stroke="#000000" points="7092.1102,-17.4294 7087.0472,-18.9879 7091.977,-20.9269 7092.1102,-17.4294"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->time --> +<g id="edge79" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->time</title> +<path fill="none" stroke="#000000" d="M6226.3419,-1049.5351C5749.066,-1041.1172 4260.7701,-1012.0232 4226,-976 4215.3452,-964.9612 4210.6564,-910.7306 4231,-846 4272.4144,-714.2246 4395,-720.13 4395,-582 4395,-582 4395,-582 4395,-488 4395,-339.5203 4423.0245,-276.8316 4542,-188 4665.8865,-95.5016 4735.101,-167.8695 4885,-130 4891.757,-128.2929 4898.8878,-126.1296 4905.6525,-123.8997"/> +<polygon fill="#000000" stroke="#000000" points="4906.5607,-125.4406 4910.7419,-122.1879 4905.4449,-122.1232 4906.5607,-125.4406"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->sync --> +<g id="edge78" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->sync</title> +<path fill="none" stroke="#000000" d="M6517.8729,-1050.1556C6946.0802,-1044.2445 8194.7998,-1023.3239 8600,-976 8850.8058,-946.7081 9157,-1116.5106 9157,-864 9157,-864 9157,-864 9157,-206 9157,-83.122 8701.7119,-32.4629 8575.2426,-20.7613"/> +<polygon fill="#000000" stroke="#000000" points="8575.3139,-19.0106 8570.1751,-20.297 8574.9945,-22.496 8575.3139,-19.0106"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->net/url --> +<g id="edge76" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->net/url</title> +<path fill="none" stroke="#000000" d="M6517.6914,-1048.4038C7042.1603,-1034.5115 8803,-979.2855 8803,-864 8803,-864 8803,-864 8803,-676 8803,-498.245 8715.5539,-295.9138 8684.1013,-228.8883"/> +<polygon fill="#000000" stroke="#000000" points="8685.6519,-228.0735 8681.9354,-224.2985 8682.4866,-229.5671 8685.6519,-228.0735"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client --> +<g id="edge72" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client</title> +<path fill="none" stroke="#000000" d="M6372,-1033.8759C6372,-1018.9211 6372,-997.5983 6372,-981.3629"/> +<polygon fill="#000000" stroke="#000000" points="6373.7501,-981.0685 6372,-976.0685 6370.2501,-981.0685 6373.7501,-981.0685"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client/auth/challenge --> +<g id="edge73" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client/auth/challenge</title> +<path fill="none" stroke="#000000" d="M6517.7862,-1046.1637C7008.012,-1025.8865 8570.6629,-955.6994 8648,-882 8745.255,-789.3195 8685,-716.3436 8685,-582 8685,-582 8685,-582 8685,-488 8685,-418.0804 8480.3356,-351.2901 8366.0226,-319.4382"/> +<polygon fill="#000000" stroke="#000000" points="8366.2712,-317.6912 8360.9853,-318.0409 8365.3356,-321.0638 8366.2712,-317.6912"/> +</g> +<!-- github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client/transport --> +<g id="edge74" class="edge"> +<title>github.com/docker/distribution/registry/client/auth->github.com/docker/distribution/registry/client/transport</title> +<path fill="none" stroke="#000000" d="M6226.2491,-1049.4608C5753.214,-1040.8781 4289.2423,-1011.4937 4255,-976 4214.8845,-934.4185 4226.1654,-896.0683 4255,-846 4296.5432,-773.8644 4340.2222,-784.1645 4417,-752 4798.2811,-592.2702 4919.9009,-630.7991 5314,-506 5431.5412,-468.7783 5504.1112,-512.877 5575,-412 5593.4147,-385.7953 5584.1195,-347.1783 5575.068,-322.8209"/> +<polygon fill="#000000" stroke="#000000" points="5576.6985,-322.1851 5573.2692,-318.1475 5573.4321,-323.4424 5576.6985,-322.1851"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->bytes --> +<g id="edge150" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->bytes</title> +<path fill="none" stroke="#000000" d="M2745.4271,-391.6258C2355.5569,-384.7021 1307.1407,-362.172 1157,-318 992.7815,-269.6863 853.1326,-101.8235 806.5441,-40.5832"/> +<polygon fill="#000000" stroke="#000000" points="807.7632,-39.2939 803.3515,-36.3613 804.9715,-41.405 807.7632,-39.2939"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->encoding/json --> +<g id="edge151" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->encoding/json</title> +<path fill="none" stroke="#000000" d="M2745.3216,-391.4202C2328.5943,-383.5889 1150.6456,-358.1414 981,-318 724.4301,-257.2907 445.5826,-96.2019 351.2761,-38.7324"/> +<polygon fill="#000000" stroke="#000000" points="352.1048,-37.1879 346.9256,-36.0749 350.2802,-40.1748 352.1048,-37.1879"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->errors --> +<g id="edge152" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->errors</title> +<path fill="none" stroke="#000000" d="M3036.5076,-391.4024C3353.3538,-384.8957 4085.6045,-364.9329 4188,-318 4261.1455,-284.4738 4255.3458,-243.0814 4314,-188 4373.4428,-132.178 4447.3387,-71.0778 4486.212,-39.5322"/> +<polygon fill="#000000" stroke="#000000" points="4487.5369,-40.7111 4490.3194,-36.2033 4485.3331,-37.9919 4487.5369,-40.7111"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->expvar --> +<g id="edge153" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->expvar</title> +<path fill="none" stroke="#000000" d="M3005.0848,-375.9356C3076.8675,-363.1139 3170.9316,-343.6286 3252,-318 3255.1769,-316.9957 3258.4419,-315.8364 3261.6806,-314.6"/> +<polygon fill="#000000" stroke="#000000" points="3262.7242,-316.0694 3266.7317,-312.6049 3261.4384,-312.8141 3262.7242,-316.0694"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->fmt --> +<g id="edge154" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->fmt</title> +<path fill="none" stroke="#000000" d="M3036.5667,-388.6742C3345.4868,-376.9575 4058.0795,-347.6222 4299,-318 4820.1553,-253.9217 4939.9559,-173.3854 5459,-94 5703.4996,-56.6049 6000.7317,-29.2286 6097.722,-20.7556"/> +<polygon fill="#000000" stroke="#000000" points="6097.9052,-22.4963 6102.7345,-20.319 6097.6014,-19.0095 6097.9052,-22.4963"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/beorn7/perks/quantile --> +<g id="edge155" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/beorn7/perks/quantile</title> +<path fill="none" stroke="#000000" d="M2745.3707,-388.6421C2601.5313,-380.8604 2393.6688,-362.1598 2327,-318 2259.9325,-273.5761 2224.5427,-178.5804 2211.2661,-134.8706"/> +<polygon fill="#000000" stroke="#000000" points="2212.9349,-134.3427 2209.8291,-130.0512 2209.5809,-135.3429 2212.9349,-134.3427"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->math --> +<g id="edge164" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->math</title> +<path fill="none" stroke="#000000" d="M2745.3689,-385.9116C2592.2664,-375.6295 2362.7409,-354.6524 2283,-318 2170.0775,-266.0959 2126.3053,-243.6438 2076,-130 2069.5236,-115.3693 2065.8625,-106.3786 2076,-94 2119.7313,-40.6011 2338.2982,-23.9158 2421.6431,-19.4461"/> +<polygon fill="#000000" stroke="#000000" points="2422.0324,-21.1782 2426.934,-19.1691 2421.8493,-17.683 2422.0324,-21.1782"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sort --> +<g id="edge169" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sort</title> +<path fill="none" stroke="#000000" d="M2745.3091,-387.0458C2638.3123,-378.5263 2505.7965,-359.6581 2471,-318 2426.3632,-264.5612 2348.4779,-264.4865 2520,-94 2557.7633,-56.4647 2717.2181,-31.3912 2786.5652,-22.0631"/> +<polygon fill="#000000" stroke="#000000" points="2787.0727,-23.7611 2791.7979,-21.3662 2786.6106,-20.2917 2787.0727,-23.7611"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/cespare/xxhash/v2 --> +<g id="edge156" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/cespare/xxhash/v2</title> +<path fill="none" stroke="#000000" d="M2745.4167,-391.5264C2467.1661,-385.6853 1880.4694,-367.5906 1803,-318 1736.3431,-275.3309 1705.8161,-179.0106 1694.9419,-134.8733"/> +<polygon fill="#000000" stroke="#000000" points="1696.6426,-134.4605 1693.7712,-130.0088 1693.2397,-135.2795 1696.6426,-134.4605"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->strings --> +<g id="edge170" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->strings</title> +<path fill="none" stroke="#000000" d="M3036.5781,-390.5969C3311.8787,-383.3359 3927.5157,-363.1603 4444,-318 5236.1236,-248.7384 5428.4722,-179.5735 6219,-94 6533.5648,-59.9488 6915.4791,-29.2948 7027.6044,-20.512"/> +<polygon fill="#000000" stroke="#000000" points="7027.8902,-22.2451 7032.7384,-20.1103 7027.6171,-18.7557 7027.8902,-22.2451"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->time --> +<g id="edge173" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->time</title> +<path fill="none" stroke="#000000" d="M3036.5137,-389.2497C3361.4352,-378.1464 4126.4478,-349.0702 4240,-318 4300.5879,-301.4219 4431.218,-207.2832 4491,-188 4659.4519,-133.6643 4712.9402,-171.519 4885,-130 4891.7749,-128.3652 4898.9134,-126.2336 4905.6802,-124.012"/> +<polygon fill="#000000" stroke="#000000" points="4906.5877,-125.5533 4910.7702,-122.3022 4905.4732,-122.2355 4906.5877,-125.5533"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sync --> +<g id="edge171" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sync</title> +<path fill="none" stroke="#000000" d="M3036.5866,-392.9642C3909.88,-386.6378 8417.8914,-352.3377 8479,-318 8559.4284,-272.8062 8553.2478,-221.2255 8567,-130 8569.385,-114.1788 8569.4375,-109.8132 8567,-94 8564.1782,-75.6934 8557.7449,-55.7508 8552.2487,-40.889"/> +<polygon fill="#000000" stroke="#000000" points="8553.8275,-40.1156 8550.4273,-36.0534 8550.5521,-41.3493 8553.8275,-40.1156"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus->io/ioutil --> +<g id="edge163" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2745.3098,-392.9164C2342.9486,-389.2287 1222.137,-374.0765 858,-318 582.6435,-275.5955 483.0544,-296.9314 260,-130 227.7133,-105.837 202.293,-65.6788 188.4792,-40.5848"/> +<polygon fill="#000000" stroke="#000000" points="189.9587,-39.6419 186.0359,-36.0817 186.8823,-41.3111 189.9587,-39.6419"/> +</g> +<!-- github.com/golang/protobuf/proto --> +<g id="node51" class="node"> +<title>github.com/golang/protobuf/proto</title> +<g id="a_node51"><a xlink:href="https://godoc.org/github.com/golang/protobuf/proto" xlink:title="github.com/golang/protobuf/proto" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2997,-130C2997,-130 2819,-130 2819,-130 2813,-130 2807,-124 2807,-118 2807,-118 2807,-106 2807,-106 2807,-100 2813,-94 2819,-94 2819,-94 2997,-94 2997,-94 3003,-94 3009,-100 3009,-106 3009,-106 3009,-118 3009,-118 3009,-124 3003,-130 2997,-130"/> +<text text-anchor="middle" x="2908" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/golang/protobuf/proto</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/golang/protobuf/proto --> +<g id="edge157" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M2745.3403,-385.0275C2646.2783,-375.5324 2527.658,-356.3298 2496,-318 2403.268,-205.7251 2650.9011,-148.9478 2801.6176,-125.522"/> +<polygon fill="#000000" stroke="#000000" points="2802.2621,-127.1935 2806.9374,-124.7027 2801.7293,-123.7343 2802.2621,-127.1935"/> +</g> +<!-- sync/atomic --> +<g id="node53" class="node"> +<title>sync/atomic</title> +<g id="a_node53"><a xlink:href="https://godoc.org/sync/atomic" xlink:title="sync/atomic" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1985.5,-36C1985.5,-36 1926.5,-36 1926.5,-36 1920.5,-36 1914.5,-30 1914.5,-24 1914.5,-24 1914.5,-12 1914.5,-12 1914.5,-6 1920.5,0 1926.5,0 1926.5,0 1985.5,0 1985.5,0 1991.5,0 1997.5,-6 1997.5,-12 1997.5,-12 1997.5,-24 1997.5,-24 1997.5,-30 1991.5,-36 1985.5,-36"/> +<text text-anchor="middle" x="1956" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">sync/atomic</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->sync/atomic --> +<g id="edge172" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->sync/atomic</title> +<path fill="none" stroke="#000000" d="M2745.2362,-385.8993C2540.5555,-373.5308 2187.1282,-348.1677 2136,-318 2028.602,-254.6308 1977.6961,-99.9561 1961.7458,-41.18"/> +<polygon fill="#000000" stroke="#000000" points="1963.377,-40.5049 1960.3966,-36.1254 1959.9954,-41.4076 1963.377,-40.5049"/> +</g> +<!-- unicode/utf8 --> +<g id="node54" class="node"> +<title>unicode/utf8</title> +<g id="a_node54"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2099.5,-36C2099.5,-36 2038.5,-36 2038.5,-36 2032.5,-36 2026.5,-30 2026.5,-24 2026.5,-24 2026.5,-12 2026.5,-12 2026.5,-6 2032.5,0 2038.5,0 2038.5,0 2099.5,0 2099.5,0 2105.5,0 2111.5,-6 2111.5,-12 2111.5,-12 2111.5,-24 2111.5,-24 2111.5,-30 2105.5,-36 2099.5,-36"/> +<text text-anchor="middle" x="2069" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->unicode/utf8 --> +<g id="edge174" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M2745.2736,-388.2527C2573.1949,-379.6674 2298.5362,-359.7996 2205,-318 2109.3999,-275.2781 2082.5182,-232.8765 2063,-130 2057.2297,-99.5861 2060.94,-63.7543 2064.5813,-40.9626"/> +<polygon fill="#000000" stroke="#000000" points="2066.3087,-41.2424 2065.4021,-36.0231 2062.856,-40.6686 2066.3087,-41.2424"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal --> +<g id="node58" class="node"> +<title>github.com/prometheus/client_golang/prometheus/internal</title> +<g id="a_node58"><a xlink:href="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" xlink:title="github.com/prometheus/client_golang/prometheus/internal" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2835,-318C2835,-318 2523,-318 2523,-318 2517,-318 2511,-312 2511,-306 2511,-306 2511,-294 2511,-294 2511,-288 2517,-282 2523,-282 2523,-282 2835,-282 2835,-282 2841,-282 2847,-288 2847,-294 2847,-294 2847,-306 2847,-306 2847,-312 2841,-318 2835,-318"/> +<text text-anchor="middle" x="2679" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_golang/prometheus/internal</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_golang/prometheus/internal --> +<g id="edge158" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_golang/prometheus/internal</title> +<path fill="none" stroke="#000000" d="M2850.1243,-375.8759C2814.1969,-359.9458 2761.9732,-336.79 2724.6803,-320.2545"/> +<polygon fill="#000000" stroke="#000000" points="2725.0304,-318.4955 2719.7502,-318.0685 2723.6117,-321.695 2725.0304,-318.4955"/> +</g> +<!-- github.com/prometheus/client_model/go --> +<g id="node59" class="node"> +<title>github.com/prometheus/client_model/go</title> +<g id="a_node59"><a xlink:href="https://godoc.org/github.com/prometheus/client_model/go" xlink:title="github.com/prometheus/client_model/go" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3116.5,-224C3116.5,-224 2901.5,-224 2901.5,-224 2895.5,-224 2889.5,-218 2889.5,-212 2889.5,-212 2889.5,-200 2889.5,-200 2889.5,-194 2895.5,-188 2901.5,-188 2901.5,-188 3116.5,-188 3116.5,-188 3122.5,-188 3128.5,-194 3128.5,-200 3128.5,-200 3128.5,-212 3128.5,-212 3128.5,-218 3122.5,-224 3116.5,-224"/> +<text text-anchor="middle" x="3009" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/client_model/go</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_model/go --> +<g id="edge159" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M2902.3143,-375.9738C2923.9325,-341.5313 2971.0624,-266.443 2994.7742,-228.6648"/> +<polygon fill="#000000" stroke="#000000" points="2996.3706,-229.4132 2997.5465,-224.2479 2993.4062,-227.5525 2996.3706,-229.4132"/> +</g> +<!-- github.com/prometheus/common/expfmt --> +<g id="node60" class="node"> +<title>github.com/prometheus/common/expfmt</title> +<g id="a_node60"><a xlink:href="https://godoc.org/github.com/prometheus/common/expfmt" xlink:title="github.com/prometheus/common/expfmt" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M4162,-318C4162,-318 3944,-318 3944,-318 3938,-318 3932,-312 3932,-306 3932,-306 3932,-294 3932,-294 3932,-288 3938,-282 3944,-282 3944,-282 4162,-282 4162,-282 4168,-282 4174,-288 4174,-294 4174,-294 4174,-306 4174,-306 4174,-312 4168,-318 4162,-318"/> +<text text-anchor="middle" x="4053" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/expfmt</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/expfmt --> +<g id="edge160" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/expfmt</title> +<path fill="none" stroke="#000000" d="M3036.6979,-382.2138C3266.5591,-363.6191 3709.4975,-327.7876 3926.6805,-310.2186"/> +<polygon fill="#000000" stroke="#000000" points="3926.926,-311.9546 3931.7686,-309.807 3926.6437,-308.466 3926.926,-311.9546"/> +</g> +<!-- github.com/prometheus/common/model --> +<g id="node61" class="node"> +<title>github.com/prometheus/common/model</title> +<g id="a_node61"><a xlink:href="https://godoc.org/github.com/prometheus/common/model" xlink:title="github.com/prometheus/common/model" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M3501.5,-224C3501.5,-224 3288.5,-224 3288.5,-224 3282.5,-224 3276.5,-218 3276.5,-212 3276.5,-212 3276.5,-200 3276.5,-200 3276.5,-194 3282.5,-188 3288.5,-188 3288.5,-188 3501.5,-188 3501.5,-188 3507.5,-188 3513.5,-194 3513.5,-200 3513.5,-200 3513.5,-212 3513.5,-212 3513.5,-218 3507.5,-224 3501.5,-224"/> +<text text-anchor="middle" x="3395" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/model</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/model --> +<g id="edge161" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/common/model</title> +<path fill="none" stroke="#000000" d="M2909.3201,-375.7891C2935.6648,-350.6881 2986.843,-305.8615 3039,-282 3112.24,-248.4931 3200.7845,-229.5124 3271.2453,-218.8988"/> +<polygon fill="#000000" stroke="#000000" points="3271.5986,-220.6156 3276.2868,-218.149 3271.0837,-217.1536 3271.5986,-220.6156"/> +</g> +<!-- github.com/prometheus/procfs --> +<g id="node62" class="node"> +<title>github.com/prometheus/procfs</title> +<g id="a_node62"><a xlink:href="https://godoc.org/github.com/prometheus/procfs" xlink:title="github.com/prometheus/procfs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2848,-224C2848,-224 2688,-224 2688,-224 2682,-224 2676,-218 2676,-212 2676,-212 2676,-200 2676,-200 2676,-194 2682,-188 2688,-188 2688,-188 2848,-188 2848,-188 2854,-188 2860,-194 2860,-200 2860,-200 2860,-212 2860,-212 2860,-218 2854,-224 2848,-224"/> +<text text-anchor="middle" x="2768" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->github.com/prometheus/procfs --> +<g id="edge162" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->github.com/prometheus/procfs</title> +<path fill="none" stroke="#000000" d="M2889.589,-375.943C2887.0373,-352.5875 2880.0097,-311.4186 2861,-282 2846.4811,-259.531 2823.1568,-240.4949 2803.4995,-227.0942"/> +<polygon fill="#000000" stroke="#000000" points="2804.2356,-225.4809 2799.1077,-224.1513 2802.2873,-228.3885 2804.2356,-225.4809"/> +</g> +<!-- os --> +<g id="node63" class="node"> +<title>os</title> +<g id="a_node63"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M935,-36C935,-36 905,-36 905,-36 899,-36 893,-30 893,-24 893,-24 893,-12 893,-12 893,-6 899,0 905,0 905,0 935,0 935,0 941,0 947,-6 947,-12 947,-12 947,-24 947,-24 947,-30 941,-36 935,-36"/> +<text text-anchor="middle" x="920" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->os --> +<g id="edge165" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->os</title> +<path fill="none" stroke="#000000" d="M2745.2218,-391.5894C2528.5925,-386.3475 2109.3667,-369.8191 1757,-318 1386.65,-263.5362 1189.455,-405.4741 936,-130 914.0236,-106.1144 913.7518,-66.3988 916.3211,-41.2416"/> +<polygon fill="#000000" stroke="#000000" points="918.0789,-41.2711 916.9056,-36.1053 914.6014,-40.8753 918.0789,-41.2711"/> +</g> +<!-- path/filepath --> +<g id="node64" class="node"> +<title>path/filepath</title> +<g id="a_node64"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M721.5,-36C721.5,-36 660.5,-36 660.5,-36 654.5,-36 648.5,-30 648.5,-24 648.5,-24 648.5,-12 648.5,-12 648.5,-6 654.5,0 660.5,0 660.5,0 721.5,0 721.5,0 727.5,0 733.5,-6 733.5,-12 733.5,-12 733.5,-24 733.5,-24 733.5,-30 727.5,-36 721.5,-36"/> +<text text-anchor="middle" x="691" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->path/filepath --> +<g id="edge166" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->path/filepath</title> +<path fill="none" stroke="#000000" d="M2745.2057,-392.837C2340.4356,-388.9197 1222.2875,-373.1784 1066,-318 903.2361,-260.535 758.4737,-99.8093 709.0477,-40.4234"/> +<polygon fill="#000000" stroke="#000000" points="710.1915,-39.0608 705.6548,-36.3255 707.4956,-41.2929 710.1915,-39.0608"/> +</g> +<!-- runtime --> +<g id="node65" class="node"> +<title>runtime</title> +<g id="a_node65"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3100.5,-318C3100.5,-318 3065.5,-318 3065.5,-318 3059.5,-318 3053.5,-312 3053.5,-306 3053.5,-306 3053.5,-294 3053.5,-294 3053.5,-288 3059.5,-282 3065.5,-282 3065.5,-282 3100.5,-282 3100.5,-282 3106.5,-282 3112.5,-288 3112.5,-294 3112.5,-294 3112.5,-306 3112.5,-306 3112.5,-312 3106.5,-318 3100.5,-318"/> +<text text-anchor="middle" x="3083" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->runtime --> +<g id="edge167" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->runtime</title> +<path fill="none" stroke="#000000" d="M2928.0195,-375.8759C2962.8135,-358.8413 3014.4841,-333.5443 3048.4501,-316.915"/> +<polygon fill="#000000" stroke="#000000" points="3049.6077,-318.2968 3053.3289,-314.5265 3048.0687,-315.1533 3049.6077,-318.2968"/> +</g> +<!-- runtime/debug --> +<g id="node66" class="node"> +<title>runtime/debug</title> +<g id="a_node66"><a xlink:href="https://godoc.org/runtime/debug" xlink:title="runtime/debug" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3226,-318C3226,-318 3154,-318 3154,-318 3148,-318 3142,-312 3142,-306 3142,-306 3142,-294 3142,-294 3142,-288 3148,-282 3154,-282 3154,-282 3226,-282 3226,-282 3232,-282 3238,-288 3238,-294 3238,-294 3238,-306 3238,-306 3238,-312 3232,-318 3226,-318"/> +<text text-anchor="middle" x="3190" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime/debug</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus->runtime/debug --> +<g id="edge168" class="edge"> +<title>github.com/prometheus/client_golang/prometheus->runtime/debug</title> +<path fill="none" stroke="#000000" d="M2948.2964,-375.9871C3002.67,-358.893 3083.7403,-333.406 3136.7412,-316.7436"/> +<polygon fill="#000000" stroke="#000000" points="3137.5358,-318.3283 3141.7808,-315.1592 3136.4861,-314.9894 3137.5358,-318.3283"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->bufio --> +<g id="edge177" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->bufio</title> +<path fill="none" stroke="#000000" d="M5126.528,-469.5702C5123.7863,-405.2441 5105.1681,-189.4326 4979,-94 4943.4257,-67.0919 4264.15,-28.4404 4105.7989,-19.7703"/> +<polygon fill="#000000" stroke="#000000" points="4105.4756,-18.0001 4100.3876,-19.4746 4105.2846,-21.4949 4105.4756,-18.0001"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->compress/gzip --> +<g id="edge178" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->compress/gzip</title> +<path fill="none" stroke="#000000" d="M5182.3802,-469.9871C5234.6177,-452.9963 5312.3482,-427.7137 5363.5908,-411.0466"/> +<polygon fill="#000000" stroke="#000000" points="5364.2519,-412.6719 5368.4653,-409.4611 5363.1692,-409.3435 5364.2519,-412.6719"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->crypto/tls --> +<g id="edge179" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->crypto/tls</title> +<path fill="none" stroke="#000000" d="M5228.1612,-469.9592C5298.7914,-456.426 5394.9224,-436.1268 5478,-412 5481.1984,-411.0711 5484.4883,-410.038 5487.7776,-408.9497"/> +<polygon fill="#000000" stroke="#000000" points="5488.7597,-410.4646 5492.9325,-407.2012 5487.6353,-407.1501 5488.7597,-410.4646"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->errors --> +<g id="edge180" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->errors</title> +<path fill="none" stroke="#000000" d="M5127.8886,-469.8462C5129.3354,-447.4343 5133.007,-408.319 5142,-376 5165.7391,-290.6863 5246.4436,-267.7751 5208,-188 5178.8918,-127.597 5151.7103,-120.2229 5090,-94 4990.2079,-51.5948 4652.1766,-26.8737 4545.5216,-19.9908"/> +<polygon fill="#000000" stroke="#000000" points="4545.4218,-18.2309 4540.3201,-19.6574 4545.1979,-21.7237 4545.4218,-18.2309"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->fmt --> +<g id="edge181" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->fmt</title> +<path fill="none" stroke="#000000" d="M5138.0653,-469.8163C5153.2846,-445.9495 5182.6162,-403.8311 5216,-376 5374.5087,-243.8555 5877.2661,-101.9599 5900,-94 5970.322,-69.3779 6053.4709,-42.4348 6098.01,-28.1766"/> +<polygon fill="#000000" stroke="#000000" points="6098.7554,-29.7756 6102.9846,-26.5854 6097.6891,-26.4419 6098.7554,-29.7756"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->io --> +<g id="edge185" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->io</title> +<path fill="none" stroke="#000000" d="M5131.0425,-469.8075C5136.5412,-447.0179 5147.6093,-407.1761 5164,-376 5179.057,-347.3608 5189.6037,-344.6175 5208,-318 5236.0443,-277.4229 5253.3566,-271.4009 5267,-224 5276.9241,-189.5208 5258.1752,-100.0412 5253,-94 5225.1906,-61.5374 5101.0136,-34.8869 5041.2903,-23.69"/> +<polygon fill="#000000" stroke="#000000" points="5041.2989,-21.9117 5036.0633,-22.7186 5040.6594,-25.3528 5041.2989,-21.9117"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net/http --> +<g id="edge187" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net/http</title> +<path fill="none" stroke="#000000" d="M5299.6741,-472.9192C5384.4059,-461.9733 5486.934,-443.3613 5575,-412 5708.325,-364.5215 5849.506,-269.5817 5908.9503,-227.2369"/> +<polygon fill="#000000" stroke="#000000" points="5910.0372,-228.6112 5913.0878,-224.2803 5908.0023,-225.7635 5910.0372,-228.6112"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->strings --> +<g id="edge190" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->strings</title> +<path fill="none" stroke="#000000" d="M5299.5217,-483.4767C5698.7815,-472.5067 6657.2623,-443.0922 6720,-412 6836.4556,-354.2857 6821.466,-284.9779 6908,-188 6931.6521,-161.4933 6939.1431,-156.3225 6963,-130 6990.8085,-99.3175 7021.9998,-62.9353 7041.3233,-40.1544"/> +<polygon fill="#000000" stroke="#000000" points="7042.8925,-41.0095 7044.7898,-36.0634 7040.2222,-38.7468 7042.8925,-41.0095"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->time --> +<g id="edge192" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->time</title> +<path fill="none" stroke="#000000" d="M5068.1432,-469.9389C5039.2521,-458.0558 5006.5405,-439.5059 4987,-412 4925.5388,-325.4849 4930.4007,-189.7752 4935.3135,-135.5417"/> +<polygon fill="#000000" stroke="#000000" points="4937.0856,-135.3908 4935.8171,-130.2475 4933.6014,-135.0593 4937.0856,-135.3908"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->sync --> +<g id="edge191" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->sync</title> +<path fill="none" stroke="#000000" d="M5299.5843,-485.3276C5685.5743,-478.6998 6647.8956,-458.5578 7452,-412 7577.7786,-404.7174 8463.6114,-361.0977 8582,-318 8659.6718,-289.7247 8746.8551,-264.2735 8715,-188 8686.5089,-119.7814 8615.8373,-64.9798 8574.4904,-37.4128"/> +<polygon fill="#000000" stroke="#000000" points="8575.2279,-35.8028 8570.0909,-34.5086 8573.2996,-38.7237 8575.2279,-35.8028"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->strconv --> +<g id="edge189" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->strconv</title> +<path fill="none" stroke="#000000" d="M5082.0851,-469.9055C5047.9032,-455.5784 5000.0724,-434.3218 4960,-412 4803.531,-324.8412 4790.307,-256.9405 4625,-188 4330.3117,-65.1017 3939.0241,-28.597 3823.4673,-20.166"/> +<polygon fill="#000000" stroke="#000000" points="3823.2871,-18.3987 3818.1745,-19.7856 3823.0361,-21.8897 3823.2871,-18.3987"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_golang/prometheus --> +<g id="edge182" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_golang/prometheus</title> +<path fill="none" stroke="#000000" d="M4954.3326,-480.7412C4530.1974,-462.9108 3442.6818,-417.1923 3042.0924,-400.3518"/> +<polygon fill="#000000" stroke="#000000" points="3041.8972,-398.5922 3036.8281,-400.1305 3041.7502,-402.0891 3041.8972,-398.5922"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_model/go --> +<g id="edge183" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M4983.8976,-469.9993C4863.3525,-454.7694 4686.3632,-432.2523 4532,-412 3911.2742,-330.5612 3756.1512,-309.7047 3136,-224 3135.2417,-223.8952 3134.4809,-223.79 3133.7177,-223.6844"/> +<polygon fill="#000000" stroke="#000000" points="3133.8423,-221.935 3128.6493,-222.9817 3133.3616,-225.4018 3133.8423,-221.935"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/common/expfmt --> +<g id="edge184" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->github.com/prometheus/common/expfmt</title> +<path fill="none" stroke="#000000" d="M4954.3519,-476.0251C4852.2047,-465.6657 4721.2593,-446.631 4609,-412 4574.0509,-401.2185 4569.8908,-386.9685 4535,-376 4417.496,-339.0608 4277.9835,-319.5656 4179.3671,-309.6152"/> +<polygon fill="#000000" stroke="#000000" points="4179.2624,-307.8461 4174.1132,-309.0902 4178.9144,-311.3287 4179.2624,-307.8461"/> +</g> +<!-- net --> +<g id="node67" class="node"> +<title>net</title> +<g id="a_node67"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3493,-130C3493,-130 3463,-130 3463,-130 3457,-130 3451,-124 3451,-118 3451,-118 3451,-106 3451,-106 3451,-100 3457,-94 3463,-94 3463,-94 3493,-94 3493,-94 3499,-94 3505,-100 3505,-106 3505,-106 3505,-118 3505,-118 3505,-124 3499,-130 3493,-130"/> +<text text-anchor="middle" x="3478" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">net</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net --> +<g id="edge186" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net</title> +<path fill="none" stroke="#000000" d="M4954.4783,-471.3902C4867.2017,-460.0006 4760.4492,-441.4544 4668,-412 4633.1516,-400.8973 4627.7067,-390.1964 4594,-376 4365.975,-279.9616 4311.2416,-242.9661 4070,-188 3826.8944,-132.6092 3750.5483,-202.548 3512,-130 3511.2811,-129.7814 3510.5598,-129.5472 3509.8376,-129.2993"/> +<polygon fill="#000000" stroke="#000000" points="3510.3707,-127.6297 3505.0753,-127.4822 3509.123,-130.8997 3510.3707,-127.6297"/> +</g> +<!-- net/http/httptrace --> +<g id="node68" class="node"> +<title>net/http/httptrace</title> +<g id="a_node68"><a xlink:href="https://godoc.org/net/http/httptrace" xlink:title="net/http/httptrace" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M5327.5,-412C5327.5,-412 5242.5,-412 5242.5,-412 5236.5,-412 5230.5,-406 5230.5,-400 5230.5,-400 5230.5,-388 5230.5,-388 5230.5,-382 5236.5,-376 5242.5,-376 5242.5,-376 5327.5,-376 5327.5,-376 5333.5,-376 5339.5,-382 5339.5,-388 5339.5,-388 5339.5,-400 5339.5,-400 5339.5,-406 5333.5,-412 5327.5,-412"/> +<text text-anchor="middle" x="5285" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httptrace</text> +</a> +</g> +</g> +<!-- github.com/prometheus/client_golang/prometheus/promhttp->net/http/httptrace --> +<g id="edge188" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/promhttp->net/http/httptrace</title> +<path fill="none" stroke="#000000" d="M5157.464,-469.8759C5184.0214,-454.0759 5222.5268,-431.1676 5250.2724,-414.6607"/> +<polygon fill="#000000" stroke="#000000" points="5251.2273,-416.129 5254.6295,-412.0685 5249.4377,-413.121 5251.2273,-416.129"/> +</g> +<!-- github.com/golang/protobuf/proto->bufio --> +<g id="edge109" class="edge"> +<title>github.com/golang/protobuf/proto->bufio</title> +<path fill="none" stroke="#000000" d="M3009.045,-101.618C3035.3874,-99.0179 3063.748,-96.3127 3090,-94 3281.47,-77.132 3893.548,-31.3675 4040.878,-20.3902"/> +<polygon fill="#000000" stroke="#000000" points="4041.0653,-22.1312 4045.9215,-20.0145 4040.8052,-18.6409 4041.0653,-22.1312"/> +</g> +<!-- github.com/golang/protobuf/proto->bytes --> +<g id="edge110" class="edge"> +<title>github.com/golang/protobuf/proto->bytes</title> +<path fill="none" stroke="#000000" d="M2806.9948,-103.243C2765.7781,-99.927 2717.6341,-96.3816 2674,-94 1876.5486,-50.474 1671.1762,-129.2455 878,-36 859.3328,-33.8055 838.736,-29.6873 822.1648,-25.9433"/> +<polygon fill="#000000" stroke="#000000" points="822.4224,-24.207 817.1578,-24.7959 821.6406,-27.6186 822.4224,-24.207"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding --> +<g id="edge111" class="edge"> +<title>github.com/golang/protobuf/proto->encoding</title> +<path fill="none" stroke="#000000" d="M2908,-93.8759C2908,-78.9211 2908,-57.5983 2908,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="2909.7501,-41.0685 2908,-36.0685 2906.2501,-41.0685 2909.7501,-41.0685"/> +</g> +<!-- github.com/golang/protobuf/proto->encoding/json --> +<g id="edge112" class="edge"> +<title>github.com/golang/protobuf/proto->encoding/json</title> +<path fill="none" stroke="#000000" d="M2806.9994,-103.1577C2765.7834,-99.8291 2717.6386,-96.2974 2674,-94 1768.2213,-46.314 1540.2077,-74.6842 634,-36 540.552,-32.0109 431.6697,-25.3688 369.1905,-21.3653"/> +<polygon fill="#000000" stroke="#000000" points="369.1344,-19.6081 364.0325,-21.034 368.91,-23.1009 369.1344,-19.6081"/> +</g> +<!-- github.com/golang/protobuf/proto->errors --> +<g id="edge113" class="edge"> +<title>github.com/golang/protobuf/proto->errors</title> +<path fill="none" stroke="#000000" d="M3009.1061,-104.0215C3052.2044,-100.7313 3103.05,-96.9958 3149,-94 3680.5307,-59.3454 4329.2376,-27.0024 4480.4366,-19.5868"/> +<polygon fill="#000000" stroke="#000000" points="4480.696,-21.3263 4485.6043,-19.3335 4480.5246,-17.8305 4480.696,-21.3263"/> +</g> +<!-- github.com/golang/protobuf/proto->fmt --> +<g id="edge114" class="edge"> +<title>github.com/golang/protobuf/proto->fmt</title> +<path fill="none" stroke="#000000" d="M3009.4156,-107.8778C3105.9718,-104.023 3255.3755,-98.2401 3385,-94 4500.7318,-57.5039 5871.944,-24.1882 6097.8239,-18.7684"/> +<polygon fill="#000000" stroke="#000000" points="6097.9893,-20.5151 6102.9459,-18.6456 6097.9054,-17.0161 6097.9893,-20.5151"/> +</g> +<!-- github.com/golang/protobuf/proto->math --> +<g id="edge117" class="edge"> +<title>github.com/golang/protobuf/proto->math</title> +<path fill="none" stroke="#000000" d="M2821.0014,-93.9871C2719.1213,-72.893 2555.5139,-39.0183 2486.4395,-24.7166"/> +<polygon fill="#000000" stroke="#000000" points="2486.4872,-22.9394 2481.2362,-23.6392 2485.7775,-26.3667 2486.4872,-22.9394"/> +</g> +<!-- github.com/golang/protobuf/proto->sort --> +<g id="edge119" class="edge"> +<title>github.com/golang/protobuf/proto->sort</title> +<path fill="none" stroke="#000000" d="M2890.8399,-93.8759C2876.3113,-78.531 2855.4346,-56.4815 2839.9272,-40.1029"/> +<polygon fill="#000000" stroke="#000000" points="2840.8159,-38.4961 2836.1074,-36.0685 2838.2743,-40.9025 2840.8159,-38.4961"/> +</g> +<!-- github.com/golang/protobuf/proto->reflect --> +<g id="edge118" class="edge"> +<title>github.com/golang/protobuf/proto->reflect</title> +<path fill="none" stroke="#000000" d="M2806.9336,-104.1818C2765.7078,-101.0041 2717.5736,-97.3085 2674,-94 2330.0256,-67.8823 2235.0899,-117.9466 1900,-36 1896.7635,-35.2085 1893.4584,-34.1922 1890.1945,-33.0462"/> +<polygon fill="#000000" stroke="#000000" points="1890.4114,-31.2594 1885.115,-31.1522 1889.1885,-34.5389 1890.4114,-31.2594"/> +</g> +<!-- github.com/golang/protobuf/proto->unsafe --> +<g id="edge125" class="edge"> +<title>github.com/golang/protobuf/proto->unsafe</title> +<path fill="none" stroke="#000000" d="M2806.9455,-104.0207C2765.7215,-100.8193 2717.5854,-97.1495 2674,-94 2292.7903,-66.454 2187.195,-127.0709 1816,-36 1813.1688,-35.3054 1810.2873,-34.428 1807.4309,-33.4364"/> +<polygon fill="#000000" stroke="#000000" points="1807.6295,-31.6433 1802.333,-31.5427 1806.4107,-34.9243 1807.6295,-31.6433"/> +</g> +<!-- github.com/golang/protobuf/proto->io --> +<g id="edge115" class="edge"> +<title>github.com/golang/protobuf/proto->io</title> +<path fill="none" stroke="#000000" d="M3009.0023,-105.4918C3067.1791,-101.8553 3141.6913,-97.3881 3208,-94 3571.9973,-75.4014 4764.5826,-27.7303 4976.6135,-19.2877"/> +<polygon fill="#000000" stroke="#000000" points="4976.8042,-21.0316 4981.7306,-19.084 4976.6649,-17.5344 4976.8042,-21.0316"/> +</g> +<!-- github.com/golang/protobuf/proto->strings --> +<g id="edge121" class="edge"> +<title>github.com/golang/protobuf/proto->strings</title> +<path fill="none" stroke="#000000" d="M3009.209,-108.1555C3115.4848,-104.211 3287.47,-98.089 3436,-94 3814.7367,-83.5734 6687.9816,-25.5072 7027.298,-18.6596"/> +<polygon fill="#000000" stroke="#000000" points="7027.6089,-20.4038 7032.5726,-18.5532 7027.5383,-16.9045 7027.6089,-20.4038"/> +</g> +<!-- github.com/golang/protobuf/proto->sync --> +<g id="edge122" class="edge"> +<title>github.com/golang/protobuf/proto->sync</title> +<path fill="none" stroke="#000000" d="M3009.203,-107.9203C3115.4739,-103.7838 3287.4551,-97.5073 3436,-94 5397.8448,-47.6781 5888.9843,-74.4035 7851,-36 8105.3856,-31.0208 8412.2961,-21.9665 8510.8738,-18.9827"/> +<polygon fill="#000000" stroke="#000000" points="8511.019,-20.7292 8515.9636,-18.8284 8510.9129,-17.2308 8511.019,-20.7292"/> +</g> +<!-- github.com/golang/protobuf/proto->strconv --> +<g id="edge120" class="edge"> +<title>github.com/golang/protobuf/proto->strconv</title> +<path fill="none" stroke="#000000" d="M3009.3045,-96.8021C3016.6415,-95.8159 3023.9315,-94.8706 3031,-94 3310.8359,-59.5321 3650.186,-29.8291 3756.5065,-20.8057"/> +<polygon fill="#000000" stroke="#000000" points="3756.8562,-22.5324 3761.6905,-20.3664 3756.5606,-19.0449 3756.8562,-22.5324"/> +</g> +<!-- log --> +<g id="node52" class="node"> +<title>log</title> +<g id="a_node52"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3012,-36C3012,-36 2982,-36 2982,-36 2976,-36 2970,-30 2970,-24 2970,-24 2970,-12 2970,-12 2970,-6 2976,0 2982,0 2982,0 3012,0 3012,0 3018,0 3024,-6 3024,-12 3024,-12 3024,-24 3024,-24 3024,-30 3018,-36 3012,-36"/> +<text text-anchor="middle" x="2997" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text> +</a> +</g> +</g> +<!-- github.com/golang/protobuf/proto->log --> +<g id="edge116" class="edge"> +<title>github.com/golang/protobuf/proto->log</title> +<path fill="none" stroke="#000000" d="M2925.1601,-93.8759C2939.6887,-78.531 2960.5654,-56.4815 2976.0728,-40.1029"/> +<polygon fill="#000000" stroke="#000000" points="2977.7257,-40.9025 2979.8926,-36.0685 2975.1841,-38.4961 2977.7257,-40.9025"/> +</g> +<!-- github.com/golang/protobuf/proto->sync/atomic --> +<g id="edge123" class="edge"> +<title>github.com/golang/protobuf/proto->sync/atomic</title> +<path fill="none" stroke="#000000" d="M2806.6018,-104.0173C2580.2231,-86.1111 2050.4349,-43.7035 2012,-36 2008.9116,-35.381 2005.7545,-34.6499 2002.5901,-33.8426"/> +<polygon fill="#000000" stroke="#000000" points="2002.9024,-32.1148 1997.6203,-32.5166 2002.0001,-35.4965 2002.9024,-32.1148"/> +</g> +<!-- github.com/golang/protobuf/proto->unicode/utf8 --> +<g id="edge124" class="edge"> +<title>github.com/golang/protobuf/proto->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M2806.797,-100.6614C2624.9937,-80.2925 2249.7021,-38.2455 2116.8746,-23.3638"/> +<polygon fill="#000000" stroke="#000000" points="2117.0356,-21.621 2111.8719,-22.8033 2116.6459,-25.0992 2117.0356,-21.621"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil --> +<g id="node55" class="node"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil</title> +<g id="a_node55"><a xlink:href="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" xlink:title="github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M4043.5,-224C4043.5,-224 3732.5,-224 3732.5,-224 3726.5,-224 3720.5,-218 3720.5,-212 3720.5,-212 3720.5,-200 3720.5,-200 3720.5,-194 3726.5,-188 3732.5,-188 3732.5,-188 4043.5,-188 4043.5,-188 4049.5,-188 4055.5,-194 4055.5,-200 4055.5,-200 4055.5,-212 4055.5,-212 4055.5,-218 4049.5,-224 4043.5,-224"/> +<text text-anchor="middle" x="3888" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/matttproud/golang_protobuf_extensions/pbutil</text> +</a> +</g> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->encoding/binary --> +<g id="edge136" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->encoding/binary</title> +<path fill="none" stroke="#000000" d="M3720.4423,-196.9205C3659.8242,-193.8212 3590.8409,-190.5082 3528,-188 3420.0379,-183.6908 1665.4949,-200.9434 1584,-130 1559.0371,-108.2692 1558.6371,-66.9015 1561.3238,-40.9793"/> +<polygon fill="#000000" stroke="#000000" points="1563.0623,-41.1786 1561.8959,-36.0112 1559.5853,-40.7782 1563.0623,-41.1786"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->errors --> +<g id="edge137" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->errors</title> +<path fill="none" stroke="#000000" d="M4005.2542,-187.9658C4074.6439,-175.5071 4163.6252,-156.3919 4240,-130 4274.5686,-118.0546 4280.4394,-108.5385 4314,-94 4371.9713,-68.8868 4441.1874,-43.4474 4480.8265,-29.3"/> +<polygon fill="#000000" stroke="#000000" points="4481.8599,-30.7897 4485.9831,-27.4638 4480.6858,-27.4925 4481.8599,-30.7897"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->io --> +<g id="edge139" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->io</title> +<path fill="none" stroke="#000000" d="M3995.4862,-187.9738C4240.8222,-146.8291 4832.0188,-47.6811 4976.6602,-23.4236"/> +<polygon fill="#000000" stroke="#000000" points="4976.9742,-25.1455 4981.6158,-22.5925 4976.3952,-21.6937 4976.9742,-25.1455"/> +</g> +<!-- github.com/matttproud/golang_protobuf_extensions/pbutil->github.com/golang/protobuf/proto --> +<g id="edge138" class="edge"> +<title>github.com/matttproud/golang_protobuf_extensions/pbutil->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3720.3674,-189.921C3518.0873,-170.5186 3184.0447,-138.4778 3014.2253,-122.189"/> +<polygon fill="#000000" stroke="#000000" points="3014.1447,-120.4233 3009.0004,-121.6878 3013.8104,-123.9073 3014.1447,-120.4233"/> +</g> +<!-- github.com/opencontainers/image-spec/specs-go->fmt --> +<g id="edge146" class="edge"> +<title>github.com/opencontainers/image-spec/specs-go->fmt</title> +<path fill="none" stroke="#000000" d="M6327.243,-93.9871C6278.7427,-75.3025 6204.2135,-46.5904 6162.0198,-30.3355"/> +<polygon fill="#000000" stroke="#000000" points="6162.5837,-28.6774 6157.2889,-28.5129 6161.3255,-31.9435 6162.5837,-28.6774"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal->sort --> +<g id="edge176" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/internal->sort</title> +<path fill="none" stroke="#000000" d="M2650.3845,-281.8723C2596.3086,-245.3833 2488.923,-160.8516 2540,-94 2570.076,-54.6353 2719.5984,-30.7961 2786.5221,-21.94"/> +<polygon fill="#000000" stroke="#000000" points="2786.85,-23.6621 2791.5808,-21.2784 2786.3961,-20.1917 2786.85,-23.6621"/> +</g> +<!-- github.com/prometheus/client_golang/prometheus/internal->github.com/prometheus/client_model/go --> +<g id="edge175" class="edge"> +<title>github.com/prometheus/client_golang/prometheus/internal->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M2742.2369,-281.9871C2799.1222,-265.7834 2882.4828,-242.0382 2940.8356,-225.4165"/> +<polygon fill="#000000" stroke="#000000" points="2941.3672,-227.0848 2945.6965,-224.0319 2940.4083,-223.7187 2941.3672,-227.0848"/> +</g> +<!-- github.com/prometheus/client_model/go->fmt --> +<g id="edge193" class="edge"> +<title>github.com/prometheus/client_model/go->fmt</title> +<path fill="none" stroke="#000000" d="M3128.559,-189.9608C3232.0931,-175.6472 3385.7506,-153.387 3519,-130 3598.4977,-116.0471 3616.8239,-103.2929 3697,-94 3946.2411,-65.1114 5826.9332,-24.4153 6097.5907,-18.6814"/> +<polygon fill="#000000" stroke="#000000" points="6097.9878,-20.4235 6102.9496,-18.568 6097.9137,-16.9243 6097.9878,-20.4235"/> +</g> +<!-- github.com/prometheus/client_model/go->math --> +<g id="edge195" class="edge"> +<title>github.com/prometheus/client_model/go->math</title> +<path fill="none" stroke="#000000" d="M2928.3279,-187.9739C2874.3206,-174.7355 2802.1901,-154.7734 2741,-130 2709.6665,-117.3143 2704.9641,-107.5625 2674,-94 2608.9556,-65.5101 2529.5461,-40.3224 2486.1415,-27.3277"/> +<polygon fill="#000000" stroke="#000000" points="2486.578,-25.6318 2481.2864,-25.8801 2485.5779,-28.9859 2486.578,-25.6318"/> +</g> +<!-- github.com/prometheus/client_model/go->github.com/golang/protobuf/proto --> +<g id="edge194" class="edge"> +<title>github.com/prometheus/client_model/go->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M2989.5262,-187.8759C2972.8989,-172.401 2948.9451,-150.1073 2931.3027,-133.6877"/> +<polygon fill="#000000" stroke="#000000" points="2932.2664,-132.1939 2927.414,-130.0685 2929.8819,-134.756 2932.2664,-132.1939"/> +</g> +<!-- github.com/prometheus/common/expfmt->bufio --> +<g id="edge196" class="edge"> +<title>github.com/prometheus/common/expfmt->bufio</title> +<path fill="none" stroke="#000000" d="M4063.3033,-281.8818C4077.5121,-258.0874 4105.0242,-216.0491 4137,-188 4176.4948,-153.3553 4213.4875,-175.3562 4240,-130 4273.8598,-72.0742 4162.7447,-37.9965 4105.257,-24.6085"/> +<polygon fill="#000000" stroke="#000000" points="4105.4682,-22.8617 4100.2038,-23.4527 4104.6878,-26.2736 4105.4682,-22.8617"/> +</g> +<!-- github.com/prometheus/common/expfmt->bytes --> +<g id="edge197" class="edge"> +<title>github.com/prometheus/common/expfmt->bytes</title> +<path fill="none" stroke="#000000" d="M3931.9248,-296.1715C3336.2147,-277.0549 742.6913,-190.456 686,-130 653.6676,-95.5205 716.6875,-54.7959 758.015,-33.212"/> +<polygon fill="#000000" stroke="#000000" points="759.0468,-34.6491 762.6899,-30.8033 757.4436,-31.5378 759.0468,-34.6491"/> +</g> +<!-- github.com/prometheus/common/expfmt->fmt --> +<g id="edge198" class="edge"> +<title>github.com/prometheus/common/expfmt->fmt</title> +<path fill="none" stroke="#000000" d="M4140.4495,-281.9593C4198.5397,-268.7757 4275.9767,-248.8816 4342,-224 4375.4786,-211.3832 4380.1317,-199.5298 4414,-188 4620.6135,-117.6622 4679.5014,-121.6566 4896,-94 5371.072,-33.3119 5955.4576,-20.6791 6097.6904,-18.4355"/> +<polygon fill="#000000" stroke="#000000" points="6097.9843,-20.1813 6102.9566,-18.3541 6097.9301,-16.6817 6097.9843,-20.1813"/> +</g> +<!-- github.com/prometheus/common/expfmt->math --> +<g id="edge206" class="edge"> +<title>github.com/prometheus/common/expfmt->math</title> +<path fill="none" stroke="#000000" d="M3931.612,-297.8032C3689.3614,-292.4417 3128.9556,-275.14 2661,-224 2405.3286,-196.0593 2256.4385,-326.0785 2090,-130 2079.6459,-117.802 2079.9085,-106.4162 2090,-94 2131.7851,-42.5895 2340.5396,-24.763 2421.6753,-19.7152"/> +<polygon fill="#000000" stroke="#000000" points="2421.9476,-21.4519 2426.8319,-19.4009 2421.7347,-17.9584 2421.9476,-21.4519"/> +</g> +<!-- github.com/prometheus/common/expfmt->io --> +<g id="edge204" class="edge"> +<title>github.com/prometheus/common/expfmt->io</title> +<path fill="none" stroke="#000000" d="M4100.8741,-281.972C4163.1145,-258.8285 4275.4068,-218.075 4373,-188 4602.1352,-117.3882 4882.8244,-48.3448 4976.7031,-25.7172"/> +<polygon fill="#000000" stroke="#000000" points="4977.3741,-27.3557 4981.8256,-24.4839 4976.5548,-23.9529 4977.3741,-27.3557"/> +</g> +<!-- github.com/prometheus/common/expfmt->mime --> +<g id="edge207" class="edge"> +<title>github.com/prometheus/common/expfmt->mime</title> +<path fill="none" stroke="#000000" d="M4154.7539,-281.9871C4276.6517,-260.4082 4474.1025,-225.4545 4551.4513,-211.7619"/> +<polygon fill="#000000" stroke="#000000" points="4552.1918,-213.4081 4556.8102,-210.8133 4551.5816,-209.9617 4552.1918,-213.4081"/> +</g> +<!-- github.com/prometheus/common/expfmt->net/http --> +<g id="edge208" class="edge"> +<title>github.com/prometheus/common/expfmt->net/http</title> +<path fill="none" stroke="#000000" d="M4174.1433,-293.9589C4551.2479,-275.1537 5691.6717,-218.2837 5903.8295,-207.704"/> +<polygon fill="#000000" stroke="#000000" points="5904.0543,-209.4451 5908.9609,-207.4481 5903.8799,-205.9494 5904.0543,-209.4451"/> +</g> +<!-- github.com/prometheus/common/expfmt->strings --> +<g id="edge210" class="edge"> +<title>github.com/prometheus/common/expfmt->strings</title> +<path fill="none" stroke="#000000" d="M4174.0086,-284.3265C4527.6711,-238.5881 5544.1528,-107.6453 5695,-94 6225.8809,-45.9776 6875.8478,-23.7218 7027.3658,-18.9829"/> +<polygon fill="#000000" stroke="#000000" points="7027.6013,-20.7266 7032.5444,-18.8217 7027.4924,-17.2282 7027.6013,-20.7266"/> +</g> +<!-- github.com/prometheus/common/expfmt->sync --> +<g id="edge211" class="edge"> +<title>github.com/prometheus/common/expfmt->sync</title> +<path fill="none" stroke="#000000" d="M4174.1989,-298.4787C4783.1741,-290.7039 7486.7737,-254.5848 7662,-224 7711.4277,-215.3727 7720.7704,-201.8361 7769,-188 7942.0458,-138.3568 7987.1498,-132.5484 8163,-94 8291.4195,-65.849 8445.1975,-36.3926 8510.7969,-24.026"/> +<polygon fill="#000000" stroke="#000000" points="8511.1527,-25.7399 8515.7424,-23.0946 8510.5048,-22.3003 8511.1527,-25.7399"/> +</g> +<!-- github.com/prometheus/common/expfmt->io/ioutil --> +<g id="edge205" class="edge"> +<title>github.com/prometheus/common/expfmt->io/ioutil</title> +<path fill="none" stroke="#000000" d="M3931.9154,-296.8645C3419.4193,-283.5641 1448.9479,-232.1193 1317,-224 878.0635,-196.9903 743.5093,-285.0962 332,-130 278.4832,-109.8297 226.8049,-66.0902 198.7238,-39.6349"/> +<polygon fill="#000000" stroke="#000000" points="199.9167,-38.3543 195.0861,-36.1798 197.5063,-40.8921 199.9167,-38.3543"/> +</g> +<!-- github.com/prometheus/common/expfmt->strconv --> +<g id="edge209" class="edge"> +<title>github.com/prometheus/common/expfmt->strconv</title> +<path fill="none" stroke="#000000" d="M3937.3312,-281.9946C3842.242,-266.0122 3720.8126,-242.408 3706,-224 3695.9693,-211.5346 3695.4828,-200.0577 3706,-188 3830.7336,-44.9964 4004.2664,-273.0036 4129,-130 4139.5172,-117.9423 4138.9999,-106.49 4129,-94 4090.8984,-46.4106 3901.5917,-26.5898 3823.6335,-20.3567"/> +<polygon fill="#000000" stroke="#000000" points="3823.3382,-18.5782 3818.2165,-19.9312 3823.0641,-22.0674 3823.3382,-18.5782"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/golang/protobuf/proto --> +<g id="edge199" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/golang/protobuf/proto</title> +<path fill="none" stroke="#000000" d="M3931.7269,-284.3726C3924.3788,-283.5382 3917.0929,-282.7399 3910,-282 3599.5076,-249.6101 3509.3323,-312.6273 3210,-224 3177.5868,-214.403 3174.0971,-201.2548 3143,-188 3089.4194,-165.1619 3026.5367,-145.1626 2979.3086,-131.4195"/> +<polygon fill="#000000" stroke="#000000" points="2979.7886,-129.7366 2974.4991,-130.0258 2978.8144,-133.0983 2979.7886,-129.7366"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/matttproud/golang_protobuf_extensions/pbutil --> +<g id="edge200" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/matttproud/golang_protobuf_extensions/pbutil</title> +<path fill="none" stroke="#000000" d="M4021.1863,-281.8759C3993.4523,-266.0759 3953.241,-243.1676 3924.2662,-226.6607"/> +<polygon fill="#000000" stroke="#000000" points="3924.9267,-225.023 3919.716,-224.0685 3923.1942,-228.0641 3924.9267,-225.023"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/client_model/go --> +<g id="edge201" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/client_model/go</title> +<path fill="none" stroke="#000000" d="M3931.7324,-284.3188C3924.3829,-283.4978 3917.0952,-282.7172 3910,-282 3573.3985,-247.975 3487.5866,-258.1719 3151,-224 3145.4161,-223.4331 3139.7122,-222.8261 3133.9529,-222.1905"/> +<polygon fill="#000000" stroke="#000000" points="3133.8132,-220.4142 3128.65,-221.5991 3133.4252,-223.8926 3133.8132,-220.4142"/> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/common/model --> +<g id="edge203" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/common/model</title> +<path fill="none" stroke="#000000" d="M3931.9295,-282.7042C3814.527,-265.9324 3637.2667,-240.6095 3518.9105,-223.7015"/> +<polygon fill="#000000" stroke="#000000" points="3518.8687,-221.9278 3513.6714,-222.9531 3518.3737,-225.3927 3518.8687,-221.9278"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg --> +<g id="node69" class="node"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title> +<g id="a_node69"><a xlink:href="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" xlink:title="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M4102.5,-130C4102.5,-130 3723.5,-130 3723.5,-130 3717.5,-130 3711.5,-124 3711.5,-118 3711.5,-118 3711.5,-106 3711.5,-106 3711.5,-100 3717.5,-94 3723.5,-94 3723.5,-94 4102.5,-94 4102.5,-94 4108.5,-94 4114.5,-100 4114.5,-106 4114.5,-106 4114.5,-118 4114.5,-118 4114.5,-124 4108.5,-130 4102.5,-130"/> +<text text-anchor="middle" x="3913" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</text> +</a> +</g> +</g> +<!-- github.com/prometheus/common/expfmt->github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg --> +<g id="edge202" class="edge"> +<title>github.com/prometheus/common/expfmt->github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg</title> +<path fill="none" stroke="#000000" d="M3931.9931,-284.8003C3821.2243,-269.6448 3671.856,-245.5782 3654,-224 3613.43,-174.973 3680.716,-146.8634 3756.8482,-131.0667"/> +<polygon fill="#000000" stroke="#000000" points="3757.3241,-132.7558 3761.8747,-130.0439 3756.6261,-129.3261 3757.3241,-132.7558"/> +</g> +<!-- github.com/prometheus/common/model->encoding/json --> +<g id="edge215" class="edge"> +<title>github.com/prometheus/common/model->encoding/json</title> +<path fill="none" stroke="#000000" d="M3276.4847,-196.4411C3234.3639,-193.3335 3186.5897,-190.1382 3143,-188 2179.3698,-140.7302 1933.1561,-224.4346 973,-130 746.1167,-107.6853 479.2125,-53.0836 369.267,-29.3381"/> +<polygon fill="#000000" stroke="#000000" points="369.4079,-27.5782 364.1509,-28.2309 368.6676,-30.999 369.4079,-27.5782"/> +</g> +<!-- github.com/prometheus/common/model->fmt --> +<g id="edge216" class="edge"> +<title>github.com/prometheus/common/model->fmt</title> +<path fill="none" stroke="#000000" d="M3513.8579,-194.8106C3746.5703,-173.1679 4278.7314,-124.9805 4727,-94 5274.7415,-56.1448 5943.8744,-26.1054 6097.5163,-19.4015"/> +<polygon fill="#000000" stroke="#000000" points="6097.8443,-21.1389 6102.7633,-19.1729 6097.6919,-17.6423 6097.8443,-21.1389"/> +</g> +<!-- github.com/prometheus/common/model->math --> +<g id="edge217" class="edge"> +<title>github.com/prometheus/common/model->math</title> +<path fill="none" stroke="#000000" d="M3276.4444,-196.6988C3126.9984,-183.9989 2879.0969,-159.417 2792,-130 2761.8115,-119.8038 2759.381,-106.3316 2730,-94 2645.1546,-58.3892 2538.6744,-34.5361 2486.2292,-24.0597"/> +<polygon fill="#000000" stroke="#000000" points="2486.2648,-22.2829 2481.02,-23.0278 2485.5846,-25.7161 2486.2648,-22.2829"/> +</g> +<!-- github.com/prometheus/common/model->sort --> +<g id="edge219" class="edge"> +<title>github.com/prometheus/common/model->sort</title> +<path fill="none" stroke="#000000" d="M3386.9757,-187.6426C3374.6814,-161.9342 3348.5908,-115.904 3311,-94 3136.386,7.7468 3054.7305,-90.06 2860,-36 2857.191,-35.2202 2854.3253,-34.2826 2851.4796,-33.2502"/> +<polygon fill="#000000" stroke="#000000" points="2851.6909,-31.4572 2846.3957,-31.3031 2850.439,-34.7257 2851.6909,-31.4572"/> +</g> +<!-- github.com/prometheus/common/model->strings --> +<g id="edge221" class="edge"> +<title>github.com/prometheus/common/model->strings</title> +<path fill="none" stroke="#000000" d="M3513.5034,-198.9667C3571.5235,-195.5676 3642.412,-191.4808 3706,-188 3955.7362,-174.3297 4589.3184,-198.0252 4830,-130 4862.1536,-120.9122 4863.7727,-102.823 4896,-94 5003.4125,-64.5932 6766.5533,-24.4995 7027.7632,-18.7083"/> +<polygon fill="#000000" stroke="#000000" points="7027.9824,-20.4539 7032.9424,-18.5936 7027.9049,-16.9548 7027.9824,-20.4539"/> +</g> +<!-- github.com/prometheus/common/model->time --> +<g id="edge222" class="edge"> +<title>github.com/prometheus/common/model->time</title> +<path fill="none" stroke="#000000" d="M3513.8693,-198.8735C3571.8198,-195.4622 3642.5426,-191.3919 3706,-188 3968.8295,-173.9513 4631.069,-182.4239 4889,-130 4894.5757,-128.8667 4900.3636,-127.2068 4905.9243,-125.3466"/> +<polygon fill="#000000" stroke="#000000" points="4906.8104,-126.891 4910.9546,-123.5913 4905.6572,-123.5864 4906.8104,-126.891"/> +</g> +<!-- github.com/prometheus/common/model->regexp --> +<g id="edge218" class="edge"> +<title>github.com/prometheus/common/model->regexp</title> +<path fill="none" stroke="#000000" d="M3513.8408,-198.2807C3571.7846,-194.7317 3642.5101,-190.7168 3706,-188 4635.2846,-148.2345 4869.3497,-182.5343 5798,-130 5985.4942,-119.3933 6031.5645,-105.5977 6219,-94 6891.0971,-52.4136 7065.7829,-126.9134 7733,-36 7747.5983,-34.0109 7763.4957,-30.4676 7776.9596,-27.0478"/> +<polygon fill="#000000" stroke="#000000" points="7777.3996,-28.7417 7781.8029,-25.7966 7776.5241,-25.3529 7777.3996,-28.7417"/> +</g> +<!-- github.com/prometheus/common/model->strconv --> +<g id="edge220" class="edge"> +<title>github.com/prometheus/common/model->strconv</title> +<path fill="none" stroke="#000000" d="M3432.8743,-187.9738C3510.0594,-151.2375 3684.3973,-68.2616 3757.1466,-33.6366"/> +<polygon fill="#000000" stroke="#000000" points="3758.0268,-35.1558 3761.7894,-31.4268 3756.5226,-31.9955 3758.0268,-35.1558"/> +</g> +<!-- github.com/prometheus/common/model->unicode/utf8 --> +<g id="edge223" class="edge"> +<title>github.com/prometheus/common/model->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M3276.4459,-197.1325C3234.3215,-194.0875 3186.5551,-190.7549 3143,-188 3026.0563,-180.6033 2178.0763,-207.2858 2090,-130 2065.2889,-108.3163 2063.8107,-67.5893 2065.8154,-41.6797"/> +<polygon fill="#000000" stroke="#000000" points="2067.5862,-41.524 2066.2889,-36.3879 2064.1002,-41.212 2067.5862,-41.524"/> +</g> +<!-- github.com/prometheus/procfs->bufio --> +<g id="edge224" class="edge"> +<title>github.com/prometheus/procfs->bufio</title> +<path fill="none" stroke="#000000" d="M2860.1973,-189.4064C2922.9973,-176.6734 3007.4028,-156.7325 3079,-130 3111.7743,-117.763 3115.3641,-103.619 3149,-94 3170.9353,-87.7271 3879.1592,-32.9238 4040.5072,-20.498"/> +<polygon fill="#000000" stroke="#000000" points="4040.7174,-22.2371 4045.5683,-20.1083 4040.4487,-18.7474 4040.7174,-22.2371"/> +</g> +<!-- github.com/prometheus/procfs->bytes --> +<g id="edge225" class="edge"> +<title>github.com/prometheus/procfs->bytes</title> +<path fill="none" stroke="#000000" d="M2675.7376,-194.4557C2653.2767,-191.9852 2629.3002,-189.6312 2607,-188 2421.7165,-174.447 1110.8609,-192.748 936,-130 884.1264,-111.3854 835.8644,-66.997 809.9036,-40.0255"/> +<polygon fill="#000000" stroke="#000000" points="810.9534,-38.5904 806.2373,-36.1776 808.4194,-41.0048 810.9534,-38.5904"/> +</g> +<!-- github.com/prometheus/procfs->encoding/hex --> +<g id="edge226" class="edge"> +<title>github.com/prometheus/procfs->encoding/hex</title> +<path fill="none" stroke="#000000" d="M2738.5001,-187.8759C2712.8889,-172.1409 2675.8029,-149.356 2648.9609,-132.8648"/> +<polygon fill="#000000" stroke="#000000" points="2649.5857,-131.1949 2644.4094,-130.0685 2647.7535,-134.177 2649.5857,-131.1949"/> +</g> +<!-- github.com/prometheus/procfs->errors --> +<g id="edge227" class="edge"> +<title>github.com/prometheus/procfs->errors</title> +<path fill="none" stroke="#000000" d="M2860.2169,-189.7843C2951.9679,-173.1431 3086.072,-147.3056 3136,-130 3169.8041,-118.2831 3173.4338,-103.229 3208,-94 3271.3705,-77.0803 4284.7677,-28.7296 4480.4407,-19.524"/> +<polygon fill="#000000" stroke="#000000" points="4480.8089,-21.2587 4485.7211,-19.2758 4480.6444,-17.7626 4480.8089,-21.2587"/> +</g> +<!-- github.com/prometheus/procfs->fmt --> +<g id="edge228" class="edge"> +<title>github.com/prometheus/procfs->fmt</title> +<path fill="none" stroke="#000000" d="M2860.1849,-190.2835C2865.1936,-189.4948 2870.1568,-188.7286 2875,-188 3068.7456,-158.8554 3120.2674,-170.3161 3312,-130 3368.1586,-118.1914 3379.3046,-102.8809 3436,-94 3574.0801,-72.3709 5801.8008,-24.8989 6097.6407,-18.6774"/> +<polygon fill="#000000" stroke="#000000" points="6097.7075,-20.4264 6102.6696,-18.5717 6097.6339,-16.9272 6097.7075,-20.4264"/> +</g> +<!-- github.com/prometheus/procfs->sort --> +<g id="edge237" class="edge"> +<title>github.com/prometheus/procfs->sort</title> +<path fill="none" stroke="#000000" d="M2675.8621,-191.3063C2602.2923,-175.4564 2517.4112,-145.3188 2556,-94 2583.9218,-56.8671 2722.5829,-32.1255 2786.5585,-22.5056"/> +<polygon fill="#000000" stroke="#000000" points="2787.0846,-24.1967 2791.7728,-21.7302 2786.5697,-20.7347 2787.0846,-24.1967"/> +</g> +<!-- github.com/prometheus/procfs->io --> +<g id="edge231" class="edge"> +<title>github.com/prometheus/procfs->io</title> +<path fill="none" stroke="#000000" d="M2860.2875,-189.803C2946.605,-174.5904 3079.0962,-151.0778 3194,-130 3278.9659,-114.414 3299.2023,-104.0435 3385,-94 3548.5057,-74.86 4762.7311,-27.4873 4976.8067,-19.2355"/> +<polygon fill="#000000" stroke="#000000" points="4977.042,-20.9779 4981.9709,-19.0366 4976.9072,-17.4805 4977.042,-20.9779"/> +</g> +<!-- github.com/prometheus/procfs->strings --> +<g id="edge239" class="edge"> +<title>github.com/prometheus/procfs->strings</title> +<path fill="none" stroke="#000000" d="M2860.12,-189.7793C2865.1455,-189.1206 2870.13,-188.5203 2875,-188 3429.7715,-128.7253 3571.7371,-157.2577 4129,-130 4439.2636,-114.8239 4516.6496,-107.2837 4827,-94 5724.9205,-55.5671 6826.6242,-24.4482 7027.7206,-18.8858"/> +<polygon fill="#000000" stroke="#000000" points="7027.9071,-20.6314 7032.8568,-18.7438 7027.8103,-17.1327 7027.9071,-20.6314"/> +</g> +<!-- github.com/prometheus/procfs->time --> +<g id="edge240" class="edge"> +<title>github.com/prometheus/procfs->time</title> +<path fill="none" stroke="#000000" d="M2860.11,-189.6816C2865.1381,-189.0482 2870.1259,-188.48 2875,-188 3740.0866,-102.8082 3964.8492,-214.5365 4830,-130 4855.5711,-127.5014 4884.2468,-122.6234 4905.6204,-118.5751"/> +<polygon fill="#000000" stroke="#000000" points="4906.0329,-120.278 4910.6147,-117.6192 4905.3748,-116.8404 4906.0329,-120.278"/> +</g> +<!-- github.com/prometheus/procfs->regexp --> +<g id="edge236" class="edge"> +<title>github.com/prometheus/procfs->regexp</title> +<path fill="none" stroke="#000000" d="M2860.1088,-189.6692C2865.1372,-189.039 2870.1254,-188.4749 2875,-188 3806.0588,-97.298 4044.1339,-163.5053 4979,-130 5341.7865,-116.9979 5432.2291,-107.4301 5795,-94 6225.5645,-78.0601 7305.8456,-92.3829 7733,-36 7747.6065,-34.072 7763.5051,-30.538 7776.9677,-27.1085"/> +<polygon fill="#000000" stroke="#000000" points="7777.4097,-28.8019 7781.8105,-25.853 7776.5313,-25.4139 7777.4097,-28.8019"/> +</g> +<!-- github.com/prometheus/procfs->io/ioutil --> +<g id="edge232" class="edge"> +<title>github.com/prometheus/procfs->io/ioutil</title> +<path fill="none" stroke="#000000" d="M2675.93,-204.1347C2337.4098,-197.016 1162.0729,-169.9343 789,-130 567.1156,-106.2492 304.7835,-47.8911 211.7921,-26.2506"/> +<polygon fill="#000000" stroke="#000000" points="211.9678,-24.4946 206.7009,-25.063 211.1727,-27.9031 211.9678,-24.4946"/> +</g> +<!-- github.com/prometheus/procfs->strconv --> +<g id="edge238" class="edge"> +<title>github.com/prometheus/procfs->strconv</title> +<path fill="none" stroke="#000000" d="M2843.0279,-187.9432C2894.5743,-174.5016 2964.0704,-154.3166 3023,-130 3054.2483,-117.1058 3057.7117,-104.0091 3090,-94 3217.2742,-54.5459 3635.4287,-27.1732 3756.7358,-19.9178"/> +<polygon fill="#000000" stroke="#000000" points="3757.0601,-21.6517 3761.9472,-19.6076 3756.852,-18.1579 3757.0601,-21.6517"/> +</g> +<!-- github.com/prometheus/procfs->os --> +<g id="edge234" class="edge"> +<title>github.com/prometheus/procfs->os</title> +<path fill="none" stroke="#000000" d="M2675.7367,-194.4682C2653.2758,-191.9973 2629.2996,-189.64 2607,-188 2516.4097,-181.3378 1048.8732,-179.9425 973,-130 942.6783,-110.0412 929.3021,-67.7293 923.7064,-41.2061"/> +<polygon fill="#000000" stroke="#000000" points="925.3863,-40.6822 922.6883,-36.1233 921.9545,-41.3697 925.3863,-40.6822"/> +</g> +<!-- github.com/prometheus/procfs->path/filepath --> +<g id="edge235" class="edge"> +<title>github.com/prometheus/procfs->path/filepath</title> +<path fill="none" stroke="#000000" d="M2675.7363,-203.6399C2307.0262,-194.0371 955.0432,-157.1234 868,-130 807.8608,-111.2601 748.0675,-66.601 715.6871,-39.6794"/> +<polygon fill="#000000" stroke="#000000" points="716.4499,-38.0354 711.4939,-36.1645 714.2015,-40.7177 716.4499,-38.0354"/> +</g> +<!-- github.com/prometheus/procfs->net --> +<g id="edge233" class="edge"> +<title>github.com/prometheus/procfs->net</title> +<path fill="none" stroke="#000000" d="M2860.1616,-190.1213C2865.1764,-189.3744 2870.1472,-188.6616 2875,-188 3120.2991,-154.5599 3187.849,-186.0008 3429,-130 3434.5422,-128.713 3440.3131,-126.9746 3445.8671,-125.0838"/> +<polygon fill="#000000" stroke="#000000" points="3446.7596,-126.6249 3450.894,-123.3129 3445.5966,-123.3237 3446.7596,-126.6249"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs --> +<g id="node70" class="node"> +<title>github.com/prometheus/procfs/internal/fs</title> +<g id="a_node70"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/fs" xlink:title="github.com/prometheus/procfs/internal/fs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1218,-130C1218,-130 1000,-130 1000,-130 994,-130 988,-124 988,-118 988,-118 988,-106 988,-106 988,-100 994,-94 1000,-94 1000,-94 1218,-94 1218,-94 1224,-94 1230,-100 1230,-106 1230,-106 1230,-118 1230,-118 1230,-124 1224,-130 1218,-130"/> +<text text-anchor="middle" x="1109" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/fs</text> +</a> +</g> +</g> +<!-- github.com/prometheus/procfs->github.com/prometheus/procfs/internal/fs --> +<g id="edge229" class="edge"> +<title>github.com/prometheus/procfs->github.com/prometheus/procfs/internal/fs</title> +<path fill="none" stroke="#000000" d="M2675.7282,-194.5807C2653.2676,-192.1061 2629.2936,-189.7189 2607,-188 2002.9109,-141.4241 1848.5866,-182.6896 1245,-130 1241.808,-129.7214 1238.5759,-129.4223 1235.3168,-129.1057"/> +<polygon fill="#000000" stroke="#000000" points="1235.3235,-127.3478 1230.1748,-128.5943 1234.977,-130.8307 1235.3235,-127.3478"/> +</g> +<!-- github.com/prometheus/procfs/internal/util --> +<g id="node71" class="node"> +<title>github.com/prometheus/procfs/internal/util</title> +<g id="a_node71"><a xlink:href="https://godoc.org/github.com/prometheus/procfs/internal/util" xlink:title="github.com/prometheus/procfs/internal/util" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1498.5,-130C1498.5,-130 1271.5,-130 1271.5,-130 1265.5,-130 1259.5,-124 1259.5,-118 1259.5,-118 1259.5,-106 1259.5,-106 1259.5,-100 1265.5,-94 1271.5,-94 1271.5,-94 1498.5,-94 1498.5,-94 1504.5,-94 1510.5,-100 1510.5,-106 1510.5,-106 1510.5,-118 1510.5,-118 1510.5,-124 1504.5,-130 1498.5,-130"/> +<text text-anchor="middle" x="1385" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/prometheus/procfs/internal/util</text> +</a> +</g> +</g> +<!-- github.com/prometheus/procfs->github.com/prometheus/procfs/internal/util --> +<g id="edge230" class="edge"> +<title>github.com/prometheus/procfs->github.com/prometheus/procfs/internal/util</title> +<path fill="none" stroke="#000000" d="M2675.7137,-194.7618C2653.2536,-192.2811 2629.2835,-189.8458 2607,-188 2130.6027,-148.5382 2009.2005,-171.7691 1533,-130 1527.4724,-129.5152 1521.8327,-128.9853 1516.1385,-128.4213"/> +<polygon fill="#000000" stroke="#000000" points="1516.0454,-126.6532 1510.8954,-127.8942 1515.6952,-130.1357 1516.0454,-126.6532"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->sort --> +<g id="edge212" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->sort</title> +<path fill="none" stroke="#000000" d="M3711.2676,-99.2119C3417.0353,-80.3042 2900.5299,-46.0417 2860,-36 2857.1704,-35.2989 2854.29,-34.417 2851.4344,-33.4223"/> +<polygon fill="#000000" stroke="#000000" points="2851.6339,-31.6292 2846.3375,-31.5246 2850.4126,-34.9093 2851.6339,-31.6292"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strings --> +<g id="edge214" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strings</title> +<path fill="none" stroke="#000000" d="M4114.6463,-105.9769C4760.3299,-86.6905 6750.839,-27.2346 7027.8701,-18.9597"/> +<polygon fill="#000000" stroke="#000000" points="7028.0233,-20.706 7032.9687,-18.8074 7027.9187,-17.2076 7028.0233,-20.706"/> +</g> +<!-- github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strconv --> +<g id="edge213" class="edge"> +<title>github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg->strconv</title> +<path fill="none" stroke="#000000" d="M3889.2844,-93.8759C3868.8651,-78.2709 3839.3729,-55.7321 3817.8386,-39.275"/> +<polygon fill="#000000" stroke="#000000" points="3818.6782,-37.7141 3813.6428,-36.0685 3816.5529,-40.495 3818.6782,-37.7141"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->fmt --> +<g id="edge241" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->fmt</title> +<path fill="none" stroke="#000000" d="M1230.1613,-95.237C1235.1691,-94.7743 1240.1307,-94.3581 1245,-94 2088.3759,-31.9721 4204.4538,-49.4906 5050,-36 5463.8003,-29.3979 5966.7728,-20.8017 6097.647,-18.556"/> +<polygon fill="#000000" stroke="#000000" points="6097.9043,-20.302 6102.8735,-18.4663 6097.8442,-16.8025 6097.9043,-20.302"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->os --> +<g id="edge242" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->os</title> +<path fill="none" stroke="#000000" d="M1072.5589,-93.8759C1037.5929,-76.4854 985.3125,-50.4835 951.9372,-33.8841"/> +<polygon fill="#000000" stroke="#000000" points="952.4107,-32.1652 947.1545,-31.5054 950.8521,-35.299 952.4107,-32.1652"/> +</g> +<!-- github.com/prometheus/procfs/internal/fs->path/filepath --> +<g id="edge243" class="edge"> +<title>github.com/prometheus/procfs/internal/fs->path/filepath</title> +<path fill="none" stroke="#000000" d="M1010.328,-93.9457C937.418,-79.9477 835.9676,-59.0895 748,-36 744.9041,-35.1874 741.7267,-34.3093 738.5325,-33.3933"/> +<polygon fill="#000000" stroke="#000000" points="738.7985,-31.648 733.5084,-31.9266 737.8177,-35.0077 738.7985,-31.648"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->bytes --> +<g id="edge244" class="edge"> +<title>github.com/prometheus/procfs/internal/util->bytes</title> +<path fill="none" stroke="#000000" d="M1259.4257,-95.0623C1156.643,-80.746 1007.5985,-58.9513 878,-36 859.4922,-32.7224 838.9048,-28.5409 822.2987,-25.0334"/> +<polygon fill="#000000" stroke="#000000" points="822.5341,-23.2944 817.2797,-23.9682 821.8074,-26.7181 822.5341,-23.2944"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->strings --> +<g id="edge248" class="edge"> +<title>github.com/prometheus/procfs/internal/util->strings</title> +<path fill="none" stroke="#000000" d="M1510.69,-99.1423C1534.9898,-97.078 1560.269,-95.2214 1584,-94 3620.1343,10.8012 4132.446,-69.5255 6171,-36 6505.6463,-30.4965 6911.1019,-21.4043 7027.4023,-18.7495"/> +<polygon fill="#000000" stroke="#000000" points="7027.7618,-20.4918 7032.7204,-18.628 7027.6817,-16.9927 7027.7618,-20.4918"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->io/ioutil --> +<g id="edge245" class="edge"> +<title>github.com/prometheus/procfs/internal/util->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1259.3559,-95.5184C1254.1718,-94.9783 1249.0372,-94.469 1244,-94 806.4688,-53.2649 692.1238,-97.3209 257,-36 242.0596,-33.8945 225.7781,-30.4472 211.8267,-27.13"/> +<polygon fill="#000000" stroke="#000000" points="212.07,-25.3885 206.7989,-25.9165 211.2487,-28.7908 212.07,-25.3885"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->strconv --> +<g id="edge247" class="edge"> +<title>github.com/prometheus/procfs/internal/util->strconv</title> +<path fill="none" stroke="#000000" d="M1510.7137,-99.5584C1535.0113,-97.4567 1560.2837,-95.48 1584,-94 2033.1948,-65.9688 3515.2882,-25.3937 3756.6419,-18.8932"/> +<polygon fill="#000000" stroke="#000000" points="3756.8296,-20.6388 3761.7807,-18.7548 3756.7353,-17.1401 3756.8296,-20.6388"/> +</g> +<!-- github.com/prometheus/procfs/internal/util->os --> +<g id="edge246" class="edge"> +<title>github.com/prometheus/procfs/internal/util->os</title> +<path fill="none" stroke="#000000" d="M1295.8935,-93.9871C1191.2879,-72.841 1023.1476,-38.8514 952.7045,-24.6112"/> +<polygon fill="#000000" stroke="#000000" points="952.6484,-22.8146 947.4007,-23.5391 951.9548,-26.2452 952.6484,-22.8146"/> +</g> +<!-- syscall --> +<g id="node72" class="node"> +<title>syscall</title> +<g id="a_node72"><a xlink:href="https://godoc.org/syscall" xlink:title="syscall" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1400,-36C1400,-36 1370,-36 1370,-36 1364,-36 1358,-30 1358,-24 1358,-24 1358,-12 1358,-12 1358,-6 1364,0 1370,0 1370,0 1400,0 1400,0 1406,0 1412,-6 1412,-12 1412,-12 1412,-24 1412,-24 1412,-30 1406,-36 1400,-36"/> +<text text-anchor="middle" x="1385" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">syscall</text> +</a> +</g> +</g> +<!-- github.com/prometheus/procfs/internal/util->syscall --> +<g id="edge249" class="edge"> +<title>github.com/prometheus/procfs/internal/util->syscall</title> +<path fill="none" stroke="#000000" d="M1385,-93.8759C1385,-78.9211 1385,-57.5983 1385,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="1386.7501,-41.0685 1385,-36.0685 1383.2501,-41.0685 1386.7501,-41.0685"/> +</g> +</g> +</svg> diff --git a/images/dot/containerd.dot b/images/dot/containerd.dot new file mode 100644 index 0000000..f674396 --- /dev/null +++ b/images/dot/containerd.dot @@ -0,0 +1,316 @@ +digraph godep { +nodesep=0.4 +ranksep=0.8 +node [shape="box",style="rounded,filled"] +edge [arrowsize="0.5"] +"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"]; +"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"]; +"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"]; +"container/list" [label="container/list" color="palegreen" URL="https://godoc.org/container/list" target="_blank"]; +"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"]; +"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"]; +"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"]; +"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"]; +"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"]; +"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"]; +"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"]; +"github.com/containerd/containerd/archive/compression" [label="github.com/containerd/containerd/archive/compression" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/archive/compression" target="_blank"]; +"github.com/containerd/containerd/archive/compression" -> "bufio"; +"github.com/containerd/containerd/archive/compression" -> "bytes"; +"github.com/containerd/containerd/archive/compression" -> "compress/gzip"; +"github.com/containerd/containerd/archive/compression" -> "context"; +"github.com/containerd/containerd/archive/compression" -> "fmt"; +"github.com/containerd/containerd/archive/compression" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/archive/compression" -> "io"; +"github.com/containerd/containerd/archive/compression" -> "os"; +"github.com/containerd/containerd/archive/compression" -> "os/exec"; +"github.com/containerd/containerd/archive/compression" -> "strconv"; +"github.com/containerd/containerd/archive/compression" -> "sync"; +"github.com/containerd/containerd/content" [label="github.com/containerd/containerd/content" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/content" target="_blank"]; +"github.com/containerd/containerd/content" -> "context"; +"github.com/containerd/containerd/content" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/content" -> "github.com/opencontainers/go-digest"; +"github.com/containerd/containerd/content" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/content" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/content" -> "io"; +"github.com/containerd/containerd/content" -> "io/ioutil"; +"github.com/containerd/containerd/content" -> "math/rand"; +"github.com/containerd/containerd/content" -> "sync"; +"github.com/containerd/containerd/content" -> "time"; +"github.com/containerd/containerd/errdefs" [label="github.com/containerd/containerd/errdefs" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/errdefs" target="_blank"]; +"github.com/containerd/containerd/errdefs" -> "context"; +"github.com/containerd/containerd/errdefs" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/errdefs" -> "google.golang.org/grpc/codes"; +"github.com/containerd/containerd/errdefs" -> "google.golang.org/grpc/status"; +"github.com/containerd/containerd/errdefs" -> "strings"; +"github.com/containerd/containerd/images" [label="github.com/containerd/containerd/images" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/images" target="_blank"]; +"github.com/containerd/containerd/images" -> "context"; +"github.com/containerd/containerd/images" -> "encoding/json"; +"github.com/containerd/containerd/images" -> "fmt"; +"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/content"; +"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/images" -> "github.com/containerd/containerd/platforms"; +"github.com/containerd/containerd/images" -> "github.com/opencontainers/go-digest"; +"github.com/containerd/containerd/images" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/images" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/images" -> "golang.org/x/sync/errgroup"; +"github.com/containerd/containerd/images" -> "golang.org/x/sync/semaphore"; +"github.com/containerd/containerd/images" -> "io"; +"github.com/containerd/containerd/images" -> "sort"; +"github.com/containerd/containerd/images" -> "strings"; +"github.com/containerd/containerd/images" -> "time"; +"github.com/containerd/containerd/labels" [label="github.com/containerd/containerd/labels" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/labels" target="_blank"]; +"github.com/containerd/containerd/labels" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/labels" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/log" [label="github.com/containerd/containerd/log" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/log" target="_blank"]; +"github.com/containerd/containerd/log" -> "context"; +"github.com/containerd/containerd/log" -> "github.com/sirupsen/logrus"; +"github.com/containerd/containerd/log" -> "sync/atomic"; +"github.com/containerd/containerd/platforms" [label="github.com/containerd/containerd/platforms" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/platforms" target="_blank"]; +"github.com/containerd/containerd/platforms" -> "bufio"; +"github.com/containerd/containerd/platforms" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/platforms" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/platforms" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/platforms" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/platforms" -> "os"; +"github.com/containerd/containerd/platforms" -> "regexp"; +"github.com/containerd/containerd/platforms" -> "runtime"; +"github.com/containerd/containerd/platforms" -> "strconv"; +"github.com/containerd/containerd/platforms" -> "strings"; +"github.com/containerd/containerd/reference" [label="github.com/containerd/containerd/reference" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/reference" target="_blank"]; +"github.com/containerd/containerd/reference" -> "errors"; +"github.com/containerd/containerd/reference" -> "fmt"; +"github.com/containerd/containerd/reference" -> "github.com/opencontainers/go-digest"; +"github.com/containerd/containerd/reference" -> "net/url"; +"github.com/containerd/containerd/reference" -> "path"; +"github.com/containerd/containerd/reference" -> "regexp"; +"github.com/containerd/containerd/reference" -> "strings"; +"github.com/containerd/containerd/remotes" [label="github.com/containerd/containerd/remotes" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes" target="_blank"]; +"github.com/containerd/containerd/remotes" -> "context"; +"github.com/containerd/containerd/remotes" -> "fmt"; +"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/content"; +"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/images"; +"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/remotes" -> "github.com/containerd/containerd/platforms"; +"github.com/containerd/containerd/remotes" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/remotes" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/remotes" -> "github.com/sirupsen/logrus"; +"github.com/containerd/containerd/remotes" -> "io"; +"github.com/containerd/containerd/remotes" -> "strings"; +"github.com/containerd/containerd/remotes" -> "sync"; +"github.com/containerd/containerd/remotes/docker" [label="github.com/containerd/containerd/remotes/docker" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes/docker" target="_blank"]; +"github.com/containerd/containerd/remotes/docker" -> "bytes"; +"github.com/containerd/containerd/remotes/docker" -> "context"; +"github.com/containerd/containerd/remotes/docker" -> "encoding/base64"; +"github.com/containerd/containerd/remotes/docker" -> "encoding/json"; +"github.com/containerd/containerd/remotes/docker" -> "fmt"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/content"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/images"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/labels"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/reference"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/remotes"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/remotes/docker/schema1"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/containerd/containerd/version"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/opencontainers/go-digest"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/remotes/docker" -> "github.com/sirupsen/logrus"; +"github.com/containerd/containerd/remotes/docker" -> "golang.org/x/net/context/ctxhttp"; +"github.com/containerd/containerd/remotes/docker" -> "io"; +"github.com/containerd/containerd/remotes/docker" -> "io/ioutil"; +"github.com/containerd/containerd/remotes/docker" -> "net/http"; +"github.com/containerd/containerd/remotes/docker" -> "net/url"; +"github.com/containerd/containerd/remotes/docker" -> "path"; +"github.com/containerd/containerd/remotes/docker" -> "sort"; +"github.com/containerd/containerd/remotes/docker" -> "strings"; +"github.com/containerd/containerd/remotes/docker" -> "sync"; +"github.com/containerd/containerd/remotes/docker" -> "time"; +"github.com/containerd/containerd/remotes/docker/schema1" [label="github.com/containerd/containerd/remotes/docker/schema1" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/remotes/docker/schema1" target="_blank"]; +"github.com/containerd/containerd/remotes/docker/schema1" -> "bytes"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "context"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "encoding/base64"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "encoding/json"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "fmt"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/archive/compression"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/content"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/errdefs"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/images"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/log"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/containerd/containerd/remotes"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/go-digest"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/image-spec/specs-go"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "github.com/pkg/errors"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "golang.org/x/sync/errgroup"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "io"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "io/ioutil"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "strconv"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "strings"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "sync"; +"github.com/containerd/containerd/remotes/docker/schema1" -> "time"; +"github.com/containerd/containerd/version" [label="github.com/containerd/containerd/version" color="paleturquoise" URL="https://godoc.org/github.com/containerd/containerd/version" target="_blank"]; +"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="palegoldenrod" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"]; +"github.com/docker/distribution/registry/api/errcode" -> "encoding/json"; +"github.com/docker/distribution/registry/api/errcode" -> "fmt"; +"github.com/docker/distribution/registry/api/errcode" -> "net/http"; +"github.com/docker/distribution/registry/api/errcode" -> "sort"; +"github.com/docker/distribution/registry/api/errcode" -> "strings"; +"github.com/docker/distribution/registry/api/errcode" -> "sync"; +"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"]; +"github.com/golang/protobuf/proto" -> "bufio"; +"github.com/golang/protobuf/proto" -> "bytes"; +"github.com/golang/protobuf/proto" -> "encoding"; +"github.com/golang/protobuf/proto" -> "encoding/json"; +"github.com/golang/protobuf/proto" -> "errors"; +"github.com/golang/protobuf/proto" -> "fmt"; +"github.com/golang/protobuf/proto" -> "io"; +"github.com/golang/protobuf/proto" -> "log"; +"github.com/golang/protobuf/proto" -> "math"; +"github.com/golang/protobuf/proto" -> "os"; +"github.com/golang/protobuf/proto" -> "reflect"; +"github.com/golang/protobuf/proto" -> "sort"; +"github.com/golang/protobuf/proto" -> "strconv"; +"github.com/golang/protobuf/proto" -> "strings"; +"github.com/golang/protobuf/proto" -> "sync"; +"github.com/golang/protobuf/proto" -> "sync/atomic"; +"github.com/golang/protobuf/proto" -> "unicode/utf8"; +"github.com/golang/protobuf/proto" -> "unsafe"; +"github.com/golang/protobuf/ptypes" [label="github.com/golang/protobuf/ptypes" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes" target="_blank"]; +"github.com/golang/protobuf/ptypes" -> "errors"; +"github.com/golang/protobuf/ptypes" -> "fmt"; +"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/proto"; +"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/any"; +"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/duration"; +"github.com/golang/protobuf/ptypes" -> "github.com/golang/protobuf/ptypes/timestamp"; +"github.com/golang/protobuf/ptypes" -> "reflect"; +"github.com/golang/protobuf/ptypes" -> "strings"; +"github.com/golang/protobuf/ptypes" -> "time"; +"github.com/golang/protobuf/ptypes/any" [label="github.com/golang/protobuf/ptypes/any" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/any" target="_blank"]; +"github.com/golang/protobuf/ptypes/any" -> "fmt"; +"github.com/golang/protobuf/ptypes/any" -> "github.com/golang/protobuf/proto"; +"github.com/golang/protobuf/ptypes/any" -> "math"; +"github.com/golang/protobuf/ptypes/duration" [label="github.com/golang/protobuf/ptypes/duration" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/duration" target="_blank"]; +"github.com/golang/protobuf/ptypes/duration" -> "fmt"; +"github.com/golang/protobuf/ptypes/duration" -> "github.com/golang/protobuf/proto"; +"github.com/golang/protobuf/ptypes/duration" -> "math"; +"github.com/golang/protobuf/ptypes/timestamp" [label="github.com/golang/protobuf/ptypes/timestamp" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/ptypes/timestamp" target="_blank"]; +"github.com/golang/protobuf/ptypes/timestamp" -> "fmt"; +"github.com/golang/protobuf/ptypes/timestamp" -> "github.com/golang/protobuf/proto"; +"github.com/golang/protobuf/ptypes/timestamp" -> "math"; +"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"]; +"github.com/opencontainers/go-digest" -> "crypto"; +"github.com/opencontainers/go-digest" -> "fmt"; +"github.com/opencontainers/go-digest" -> "hash"; +"github.com/opencontainers/go-digest" -> "io"; +"github.com/opencontainers/go-digest" -> "regexp"; +"github.com/opencontainers/go-digest" -> "strings"; +"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go" -> "fmt"; +"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "time"; +"github.com/pkg/errors" [label="github.com/pkg/errors" color="palegoldenrod" URL="https://godoc.org/github.com/pkg/errors" target="_blank"]; +"github.com/pkg/errors" -> "fmt"; +"github.com/pkg/errors" -> "io"; +"github.com/pkg/errors" -> "path"; +"github.com/pkg/errors" -> "runtime"; +"github.com/pkg/errors" -> "strings"; +"github.com/sirupsen/logrus" [label="github.com/sirupsen/logrus" color="palegoldenrod" URL="https://godoc.org/github.com/sirupsen/logrus" target="_blank"]; +"github.com/sirupsen/logrus" -> "bufio"; +"github.com/sirupsen/logrus" -> "bytes"; +"github.com/sirupsen/logrus" -> "context"; +"github.com/sirupsen/logrus" -> "encoding/json"; +"github.com/sirupsen/logrus" -> "fmt"; +"github.com/sirupsen/logrus" -> "golang.org/x/sys/unix"; +"github.com/sirupsen/logrus" -> "io"; +"github.com/sirupsen/logrus" -> "log"; +"github.com/sirupsen/logrus" -> "os"; +"github.com/sirupsen/logrus" -> "reflect"; +"github.com/sirupsen/logrus" -> "runtime"; +"github.com/sirupsen/logrus" -> "sort"; +"github.com/sirupsen/logrus" -> "strings"; +"github.com/sirupsen/logrus" -> "sync"; +"github.com/sirupsen/logrus" -> "sync/atomic"; +"github.com/sirupsen/logrus" -> "time"; +"golang.org/x/net/context/ctxhttp" [label="golang.org/x/net/context/ctxhttp" color="palegoldenrod" URL="https://godoc.org/golang.org/x/net/context/ctxhttp" target="_blank"]; +"golang.org/x/net/context/ctxhttp" -> "context"; +"golang.org/x/net/context/ctxhttp" -> "io"; +"golang.org/x/net/context/ctxhttp" -> "net/http"; +"golang.org/x/net/context/ctxhttp" -> "net/url"; +"golang.org/x/net/context/ctxhttp" -> "strings"; +"golang.org/x/sync/errgroup" [label="golang.org/x/sync/errgroup" color="palegoldenrod" URL="https://godoc.org/golang.org/x/sync/errgroup" target="_blank"]; +"golang.org/x/sync/errgroup" -> "context"; +"golang.org/x/sync/errgroup" -> "sync"; +"golang.org/x/sync/semaphore" [label="golang.org/x/sync/semaphore" color="palegoldenrod" URL="https://godoc.org/golang.org/x/sync/semaphore" target="_blank"]; +"golang.org/x/sync/semaphore" -> "container/list"; +"golang.org/x/sync/semaphore" -> "context"; +"golang.org/x/sync/semaphore" -> "sync"; +"golang.org/x/sys/unix" [label="golang.org/x/sys/unix" color="paleturquoise" URL="https://godoc.org/golang.org/x/sys/unix" target="_blank"]; +"golang.org/x/sys/unix" -> "bytes"; +"golang.org/x/sys/unix" -> "runtime"; +"golang.org/x/sys/unix" -> "sort"; +"golang.org/x/sys/unix" -> "strings"; +"golang.org/x/sys/unix" -> "sync"; +"golang.org/x/sys/unix" -> "syscall"; +"golang.org/x/sys/unix" -> "time"; +"golang.org/x/sys/unix" -> "unsafe"; +"google.golang.org/genproto/googleapis/rpc/status" [label="google.golang.org/genproto/googleapis/rpc/status" color="paleturquoise" URL="https://godoc.org/google.golang.org/genproto/googleapis/rpc/status" target="_blank"]; +"google.golang.org/genproto/googleapis/rpc/status" -> "fmt"; +"google.golang.org/genproto/googleapis/rpc/status" -> "github.com/golang/protobuf/proto"; +"google.golang.org/genproto/googleapis/rpc/status" -> "github.com/golang/protobuf/ptypes/any"; +"google.golang.org/genproto/googleapis/rpc/status" -> "math"; +"google.golang.org/grpc/codes" [label="google.golang.org/grpc/codes" color="palegoldenrod" URL="https://godoc.org/google.golang.org/grpc/codes" target="_blank"]; +"google.golang.org/grpc/codes" -> "fmt"; +"google.golang.org/grpc/codes" -> "strconv"; +"google.golang.org/grpc/connectivity" [label="google.golang.org/grpc/connectivity" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/connectivity" target="_blank"]; +"google.golang.org/grpc/connectivity" -> "context"; +"google.golang.org/grpc/connectivity" -> "google.golang.org/grpc/grpclog"; +"google.golang.org/grpc/grpclog" [label="google.golang.org/grpc/grpclog" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/grpclog" target="_blank"]; +"google.golang.org/grpc/grpclog" -> "io"; +"google.golang.org/grpc/grpclog" -> "io/ioutil"; +"google.golang.org/grpc/grpclog" -> "log"; +"google.golang.org/grpc/grpclog" -> "os"; +"google.golang.org/grpc/grpclog" -> "strconv"; +"google.golang.org/grpc/internal" [label="google.golang.org/grpc/internal" color="paleturquoise" URL="https://godoc.org/google.golang.org/grpc/internal" target="_blank"]; +"google.golang.org/grpc/internal" -> "context"; +"google.golang.org/grpc/internal" -> "google.golang.org/grpc/connectivity"; +"google.golang.org/grpc/internal" -> "time"; +"google.golang.org/grpc/status" [label="google.golang.org/grpc/status" color="palegoldenrod" URL="https://godoc.org/google.golang.org/grpc/status" target="_blank"]; +"google.golang.org/grpc/status" -> "context"; +"google.golang.org/grpc/status" -> "errors"; +"google.golang.org/grpc/status" -> "fmt"; +"google.golang.org/grpc/status" -> "github.com/golang/protobuf/proto"; +"google.golang.org/grpc/status" -> "github.com/golang/protobuf/ptypes"; +"google.golang.org/grpc/status" -> "google.golang.org/genproto/googleapis/rpc/status"; +"google.golang.org/grpc/status" -> "google.golang.org/grpc/codes"; +"google.golang.org/grpc/status" -> "google.golang.org/grpc/internal"; +"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"]; +"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"]; +"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"]; +"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"]; +"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"]; +"math/rand" [label="math/rand" color="palegreen" URL="https://godoc.org/math/rand" target="_blank"]; +"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"]; +"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"]; +"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"]; +"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"]; +"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"]; +"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"]; +"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"]; +"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"]; +"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"]; +"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"]; +"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"]; +"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"]; +"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"]; +"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"]; +"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"]; +"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"]; +"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"]; +} diff --git a/images/dot/containers.dot b/images/dot/containers.dot new file mode 100644 index 0000000..3e53f84 --- /dev/null +++ b/images/dot/containers.dot @@ -0,0 +1,831 @@ +digraph godep { +rankdir="LR" +nodesep=0.4 +ranksep=0.8 +node [shape="box",style="rounded,filled"] +edge [arrowsize="0.5"] +"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"]; +"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"]; +"compress/bzip2" [label="compress/bzip2" color="palegreen" URL="https://godoc.org/compress/bzip2" target="_blank"]; +"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"]; +"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"]; +"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"]; +"crypto/ecdsa" [label="crypto/ecdsa" color="palegreen" URL="https://godoc.org/crypto/ecdsa" target="_blank"]; +"crypto/elliptic" [label="crypto/elliptic" color="palegreen" URL="https://godoc.org/crypto/elliptic" target="_blank"]; +"crypto/rand" [label="crypto/rand" color="palegreen" URL="https://godoc.org/crypto/rand" target="_blank"]; +"crypto/rsa" [label="crypto/rsa" color="palegreen" URL="https://godoc.org/crypto/rsa" target="_blank"]; +"crypto/sha256" [label="crypto/sha256" color="palegreen" URL="https://godoc.org/crypto/sha256" target="_blank"]; +"crypto/sha512" [label="crypto/sha512" color="palegreen" URL="https://godoc.org/crypto/sha512" target="_blank"]; +"crypto/tls" [label="crypto/tls" color="palegreen" URL="https://godoc.org/crypto/tls" target="_blank"]; +"crypto/x509" [label="crypto/x509" color="palegreen" URL="https://godoc.org/crypto/x509" target="_blank"]; +"crypto/x509/pkix" [label="crypto/x509/pkix" color="palegreen" URL="https://godoc.org/crypto/x509/pkix" target="_blank"]; +"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"]; +"encoding/base32" [label="encoding/base32" color="palegreen" URL="https://godoc.org/encoding/base32" target="_blank"]; +"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"]; +"encoding/binary" [label="encoding/binary" color="palegreen" URL="https://godoc.org/encoding/binary" target="_blank"]; +"encoding/hex" [label="encoding/hex" color="palegreen" URL="https://godoc.org/encoding/hex" target="_blank"]; +"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"]; +"encoding/pem" [label="encoding/pem" color="palegreen" URL="https://godoc.org/encoding/pem" target="_blank"]; +"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"]; +"expvar" [label="expvar" color="palegreen" URL="https://godoc.org/expvar" target="_blank"]; +"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"]; +"github.com/BurntSushi/toml" [label="github.com/BurntSushi/toml" color="paleturquoise" URL="https://godoc.org/github.com/BurntSushi/toml" target="_blank"]; +"github.com/BurntSushi/toml" -> "bufio"; +"github.com/BurntSushi/toml" -> "encoding"; +"github.com/BurntSushi/toml" -> "errors"; +"github.com/BurntSushi/toml" -> "fmt"; +"github.com/BurntSushi/toml" -> "io"; +"github.com/BurntSushi/toml" -> "io/ioutil"; +"github.com/BurntSushi/toml" -> "math"; +"github.com/BurntSushi/toml" -> "reflect"; +"github.com/BurntSushi/toml" -> "sort"; +"github.com/BurntSushi/toml" -> "strconv"; +"github.com/BurntSushi/toml" -> "strings"; +"github.com/BurntSushi/toml" -> "sync"; +"github.com/BurntSushi/toml" -> "time"; +"github.com/BurntSushi/toml" -> "unicode"; +"github.com/BurntSushi/toml" -> "unicode/utf8"; +"github.com/beorn7/perks/quantile" [label="github.com/beorn7/perks/quantile" color="paleturquoise" URL="https://godoc.org/github.com/beorn7/perks/quantile" target="_blank"]; +"github.com/beorn7/perks/quantile" -> "math"; +"github.com/beorn7/perks/quantile" -> "sort"; +"github.com/cespare/xxhash/v2" [label="github.com/cespare/xxhash/v2" color="paleturquoise" URL="https://godoc.org/github.com/cespare/xxhash/v2" target="_blank"]; +"github.com/cespare/xxhash/v2" -> "encoding/binary"; +"github.com/cespare/xxhash/v2" -> "errors"; +"github.com/cespare/xxhash/v2" -> "math/bits"; +"github.com/cespare/xxhash/v2" -> "reflect"; +"github.com/cespare/xxhash/v2" -> "unsafe"; +"github.com/containers/image/docker" [label="github.com/containers/image/docker" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/docker" target="_blank"]; +"github.com/containers/image/docker" -> "bytes"; +"github.com/containers/image/docker" -> "context"; +"github.com/containers/image/docker" -> "crypto/rand"; +"github.com/containers/image/docker" -> "crypto/tls"; +"github.com/containers/image/docker" -> "encoding/json"; +"github.com/containers/image/docker" -> "errors"; +"github.com/containers/image/docker" -> "fmt"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/docker/policyconfiguration"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/image"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/manifest"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/blobinfocache/none"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/docker/config"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/sysregistriesv2"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/pkg/tlsclientconfig"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/transports"; +"github.com/containers/image/docker" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/api/v2"; +"github.com/containers/image/docker" -> "github.com/docker/distribution/registry/client"; +"github.com/containers/image/docker" -> "github.com/docker/go-connections/tlsconfig"; +"github.com/containers/image/docker" -> "github.com/ghodss/yaml"; +"github.com/containers/image/docker" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/docker" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containers/image/docker" -> "github.com/pkg/errors"; +"github.com/containers/image/docker" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/docker" -> "io"; +"github.com/containers/image/docker" -> "io/ioutil"; +"github.com/containers/image/docker" -> "mime"; +"github.com/containers/image/docker" -> "net/http"; +"github.com/containers/image/docker" -> "net/url"; +"github.com/containers/image/docker" -> "os"; +"github.com/containers/image/docker" -> "path"; +"github.com/containers/image/docker" -> "path/filepath"; +"github.com/containers/image/docker" -> "strconv"; +"github.com/containers/image/docker" -> "strings"; +"github.com/containers/image/docker" -> "sync"; +"github.com/containers/image/docker" -> "time"; +"github.com/containers/image/v5/docker/policyconfiguration" [label="github.com/containers/image/v5/docker/policyconfiguration" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/docker/policyconfiguration" target="_blank"]; +"github.com/containers/image/v5/docker/policyconfiguration" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/v5/docker/policyconfiguration" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/docker/policyconfiguration" -> "strings"; +"github.com/containers/image/v5/docker/reference" [label="github.com/containers/image/v5/docker/reference" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/docker/reference" target="_blank"]; +"github.com/containers/image/v5/docker/reference" -> "errors"; +"github.com/containers/image/v5/docker/reference" -> "fmt"; +"github.com/containers/image/v5/docker/reference" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/v5/docker/reference" -> "path"; +"github.com/containers/image/v5/docker/reference" -> "regexp"; +"github.com/containers/image/v5/docker/reference" -> "strings"; +"github.com/containers/image/v5/image" [label="github.com/containers/image/v5/image" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/image" target="_blank"]; +"github.com/containers/image/v5/image" -> "bytes"; +"github.com/containers/image/v5/image" -> "context"; +"github.com/containers/image/v5/image" -> "crypto/sha256"; +"github.com/containers/image/v5/image" -> "encoding/hex"; +"github.com/containers/image/v5/image" -> "encoding/json"; +"github.com/containers/image/v5/image" -> "fmt"; +"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/manifest"; +"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/pkg/blobinfocache/none"; +"github.com/containers/image/v5/image" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/image" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/v5/image" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containers/image/v5/image" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/image" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/image" -> "io/ioutil"; +"github.com/containers/image/v5/image" -> "strings"; +"github.com/containers/image/v5/internal/pkg/keyctl" [label="github.com/containers/image/v5/internal/pkg/keyctl" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/internal/pkg/keyctl" target="_blank"]; +"github.com/containers/image/v5/internal/pkg/keyctl" -> "golang.org/x/sys/unix"; +"github.com/containers/image/v5/internal/pkg/keyctl" -> "unsafe"; +"github.com/containers/image/v5/manifest" [label="github.com/containers/image/v5/manifest" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/manifest" target="_blank"]; +"github.com/containers/image/v5/manifest" -> "encoding/json"; +"github.com/containers/image/v5/manifest" -> "fmt"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/pkg/compression"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/pkg/strslice"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/libtrust"; +"github.com/containers/image/v5/manifest" -> "github.com/containers/ocicrypt/spec"; +"github.com/containers/image/v5/manifest" -> "github.com/docker/docker/api/types/versions"; +"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/image-spec/specs-go"; +"github.com/containers/image/v5/manifest" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containers/image/v5/manifest" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/manifest" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/manifest" -> "regexp"; +"github.com/containers/image/v5/manifest" -> "runtime"; +"github.com/containers/image/v5/manifest" -> "strings"; +"github.com/containers/image/v5/manifest" -> "time"; +"github.com/containers/image/v5/pkg/blobinfocache/none" [label="github.com/containers/image/v5/pkg/blobinfocache/none" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/blobinfocache/none" target="_blank"]; +"github.com/containers/image/v5/pkg/blobinfocache/none" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/pkg/blobinfocache/none" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/v5/pkg/compression" [label="github.com/containers/image/v5/pkg/compression" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression" target="_blank"]; +"github.com/containers/image/v5/pkg/compression" -> "bytes"; +"github.com/containers/image/v5/pkg/compression" -> "compress/bzip2"; +"github.com/containers/image/v5/pkg/compression" -> "fmt"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/containers/image/v5/pkg/compression/internal"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/containers/image/v5/pkg/compression/types"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/klauspost/compress/zstd"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/klauspost/pgzip"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/pkg/compression" -> "github.com/ulikunitz/xz"; +"github.com/containers/image/v5/pkg/compression" -> "io"; +"github.com/containers/image/v5/pkg/compression" -> "io/ioutil"; +"github.com/containers/image/v5/pkg/compression/internal" [label="github.com/containers/image/v5/pkg/compression/internal" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression/internal" target="_blank"]; +"github.com/containers/image/v5/pkg/compression/internal" -> "io"; +"github.com/containers/image/v5/pkg/compression/types" [label="github.com/containers/image/v5/pkg/compression/types" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/compression/types" target="_blank"]; +"github.com/containers/image/v5/pkg/compression/types" -> "github.com/containers/image/v5/pkg/compression/internal"; +"github.com/containers/image/v5/pkg/docker/config" [label="github.com/containers/image/v5/pkg/docker/config" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/docker/config" target="_blank"]; +"github.com/containers/image/v5/pkg/docker/config" -> "encoding/base64"; +"github.com/containers/image/v5/pkg/docker/config" -> "encoding/json"; +"github.com/containers/image/v5/pkg/docker/config" -> "fmt"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/containers/image/v5/internal/pkg/keyctl"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker-credential-helpers/client"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker-credential-helpers/credentials"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/docker/docker/pkg/homedir"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/pkg/docker/config" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/pkg/docker/config" -> "io/ioutil"; +"github.com/containers/image/v5/pkg/docker/config" -> "os"; +"github.com/containers/image/v5/pkg/docker/config" -> "path/filepath"; +"github.com/containers/image/v5/pkg/docker/config" -> "strings"; +"github.com/containers/image/v5/pkg/strslice" [label="github.com/containers/image/v5/pkg/strslice" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/strslice" target="_blank"]; +"github.com/containers/image/v5/pkg/strslice" -> "encoding/json"; +"github.com/containers/image/v5/pkg/sysregistriesv2" [label="github.com/containers/image/v5/pkg/sysregistriesv2" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/sysregistriesv2" target="_blank"]; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "fmt"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/BurntSushi/toml"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "io/ioutil"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "os"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "path/filepath"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "regexp"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "strings"; +"github.com/containers/image/v5/pkg/sysregistriesv2" -> "sync"; +"github.com/containers/image/v5/pkg/tlsclientconfig" [label="github.com/containers/image/v5/pkg/tlsclientconfig" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/pkg/tlsclientconfig" target="_blank"]; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "crypto/tls"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/docker/go-connections/sockets"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/docker/go-connections/tlsconfig"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/pkg/errors"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "github.com/sirupsen/logrus"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "io/ioutil"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "net"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "net/http"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "os"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "path/filepath"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "strings"; +"github.com/containers/image/v5/pkg/tlsclientconfig" -> "time"; +"github.com/containers/image/v5/transports" [label="github.com/containers/image/v5/transports" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/transports" target="_blank"]; +"github.com/containers/image/v5/transports" -> "fmt"; +"github.com/containers/image/v5/transports" -> "github.com/containers/image/v5/types"; +"github.com/containers/image/v5/transports" -> "sort"; +"github.com/containers/image/v5/transports" -> "sync"; +"github.com/containers/image/v5/types" [label="github.com/containers/image/v5/types" color="paleturquoise" URL="https://godoc.org/github.com/containers/image/v5/types" target="_blank"]; +"github.com/containers/image/v5/types" -> "context"; +"github.com/containers/image/v5/types" -> "github.com/containers/image/v5/docker/reference"; +"github.com/containers/image/v5/types" -> "github.com/containers/image/v5/pkg/compression/types"; +"github.com/containers/image/v5/types" -> "github.com/opencontainers/go-digest"; +"github.com/containers/image/v5/types" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/containers/image/v5/types" -> "io"; +"github.com/containers/image/v5/types" -> "time"; +"github.com/containers/libtrust" [label="github.com/containers/libtrust" color="paleturquoise" URL="https://godoc.org/github.com/containers/libtrust" target="_blank"]; +"github.com/containers/libtrust" -> "bytes"; +"github.com/containers/libtrust" -> "crypto"; +"github.com/containers/libtrust" -> "crypto/ecdsa"; +"github.com/containers/libtrust" -> "crypto/elliptic"; +"github.com/containers/libtrust" -> "crypto/rand"; +"github.com/containers/libtrust" -> "crypto/rsa"; +"github.com/containers/libtrust" -> "crypto/sha256"; +"github.com/containers/libtrust" -> "crypto/sha512"; +"github.com/containers/libtrust" -> "crypto/tls"; +"github.com/containers/libtrust" -> "crypto/x509"; +"github.com/containers/libtrust" -> "crypto/x509/pkix"; +"github.com/containers/libtrust" -> "encoding/base32"; +"github.com/containers/libtrust" -> "encoding/base64"; +"github.com/containers/libtrust" -> "encoding/binary"; +"github.com/containers/libtrust" -> "encoding/json"; +"github.com/containers/libtrust" -> "encoding/pem"; +"github.com/containers/libtrust" -> "errors"; +"github.com/containers/libtrust" -> "fmt"; +"github.com/containers/libtrust" -> "io"; +"github.com/containers/libtrust" -> "io/ioutil"; +"github.com/containers/libtrust" -> "math/big"; +"github.com/containers/libtrust" -> "net"; +"github.com/containers/libtrust" -> "net/url"; +"github.com/containers/libtrust" -> "os"; +"github.com/containers/libtrust" -> "path"; +"github.com/containers/libtrust" -> "path/filepath"; +"github.com/containers/libtrust" -> "sort"; +"github.com/containers/libtrust" -> "strings"; +"github.com/containers/libtrust" -> "sync"; +"github.com/containers/libtrust" -> "time"; +"github.com/containers/libtrust" -> "unicode"; +"github.com/containers/ocicrypt/spec" [label="github.com/containers/ocicrypt/spec" color="paleturquoise" URL="https://godoc.org/github.com/containers/ocicrypt/spec" target="_blank"]; +"github.com/docker/distribution" [label="github.com/docker/distribution" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution" target="_blank"]; +"github.com/docker/distribution" -> "context"; +"github.com/docker/distribution" -> "errors"; +"github.com/docker/distribution" -> "fmt"; +"github.com/docker/distribution" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/docker/distribution" -> "io"; +"github.com/docker/distribution" -> "mime"; +"github.com/docker/distribution" -> "net/http"; +"github.com/docker/distribution" -> "strings"; +"github.com/docker/distribution" -> "time"; +"github.com/docker/distribution/digestset" [label="github.com/docker/distribution/digestset" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/digestset" target="_blank"]; +"github.com/docker/distribution/digestset" -> "errors"; +"github.com/docker/distribution/digestset" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/digestset" -> "sort"; +"github.com/docker/distribution/digestset" -> "strings"; +"github.com/docker/distribution/digestset" -> "sync"; +"github.com/docker/distribution/metrics" [label="github.com/docker/distribution/metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/metrics" target="_blank"]; +"github.com/docker/distribution/metrics" -> "github.com/docker/go-metrics"; +"github.com/docker/distribution/reference" [label="github.com/docker/distribution/reference" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/reference" target="_blank"]; +"github.com/docker/distribution/reference" -> "errors"; +"github.com/docker/distribution/reference" -> "fmt"; +"github.com/docker/distribution/reference" -> "github.com/docker/distribution/digestset"; +"github.com/docker/distribution/reference" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/reference" -> "path"; +"github.com/docker/distribution/reference" -> "regexp"; +"github.com/docker/distribution/reference" -> "strings"; +"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"]; +"github.com/docker/distribution/registry/api/errcode" -> "encoding/json"; +"github.com/docker/distribution/registry/api/errcode" -> "fmt"; +"github.com/docker/distribution/registry/api/errcode" -> "net/http"; +"github.com/docker/distribution/registry/api/errcode" -> "sort"; +"github.com/docker/distribution/registry/api/errcode" -> "strings"; +"github.com/docker/distribution/registry/api/errcode" -> "sync"; +"github.com/docker/distribution/registry/api/v2" [label="github.com/docker/distribution/registry/api/v2" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/v2" target="_blank"]; +"github.com/docker/distribution/registry/api/v2" -> "fmt"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/gorilla/mux"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/api/v2" -> "net/http"; +"github.com/docker/distribution/registry/api/v2" -> "net/url"; +"github.com/docker/distribution/registry/api/v2" -> "regexp"; +"github.com/docker/distribution/registry/api/v2" -> "strings"; +"github.com/docker/distribution/registry/api/v2" -> "unicode"; +"github.com/docker/distribution/registry/client" [label="github.com/docker/distribution/registry/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client" target="_blank"]; +"github.com/docker/distribution/registry/client" -> "bytes"; +"github.com/docker/distribution/registry/client" -> "context"; +"github.com/docker/distribution/registry/client" -> "encoding/json"; +"github.com/docker/distribution/registry/client" -> "errors"; +"github.com/docker/distribution/registry/client" -> "fmt"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/v2"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/auth/challenge"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/transport"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache/memory"; +"github.com/docker/distribution/registry/client" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/client" -> "io"; +"github.com/docker/distribution/registry/client" -> "io/ioutil"; +"github.com/docker/distribution/registry/client" -> "net/http"; +"github.com/docker/distribution/registry/client" -> "net/url"; +"github.com/docker/distribution/registry/client" -> "strconv"; +"github.com/docker/distribution/registry/client" -> "strings"; +"github.com/docker/distribution/registry/client" -> "time"; +"github.com/docker/distribution/registry/client/auth/challenge" [label="github.com/docker/distribution/registry/client/auth/challenge" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" target="_blank"]; +"github.com/docker/distribution/registry/client/auth/challenge" -> "fmt"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "net/http"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "net/url"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "strings"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "sync"; +"github.com/docker/distribution/registry/client/transport" [label="github.com/docker/distribution/registry/client/transport" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/transport" target="_blank"]; +"github.com/docker/distribution/registry/client/transport" -> "errors"; +"github.com/docker/distribution/registry/client/transport" -> "fmt"; +"github.com/docker/distribution/registry/client/transport" -> "io"; +"github.com/docker/distribution/registry/client/transport" -> "net/http"; +"github.com/docker/distribution/registry/client/transport" -> "regexp"; +"github.com/docker/distribution/registry/client/transport" -> "strconv"; +"github.com/docker/distribution/registry/client/transport" -> "sync"; +"github.com/docker/distribution/registry/storage/cache" [label="github.com/docker/distribution/registry/storage/cache" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache" target="_blank"]; +"github.com/docker/distribution/registry/storage/cache" -> "context"; +"github.com/docker/distribution/registry/storage/cache" -> "fmt"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution/metrics"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/storage/cache/memory" [label="github.com/docker/distribution/registry/storage/cache/memory" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" target="_blank"]; +"github.com/docker/distribution/registry/storage/cache/memory" -> "context"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/registry/storage/cache"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "sync"; +"github.com/docker/docker-credential-helpers/client" [label="github.com/docker/docker-credential-helpers/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker-credential-helpers/client" target="_blank"]; +"github.com/docker/docker-credential-helpers/client" -> "bytes"; +"github.com/docker/docker-credential-helpers/client" -> "encoding/json"; +"github.com/docker/docker-credential-helpers/client" -> "fmt"; +"github.com/docker/docker-credential-helpers/client" -> "github.com/docker/docker-credential-helpers/credentials"; +"github.com/docker/docker-credential-helpers/client" -> "io"; +"github.com/docker/docker-credential-helpers/client" -> "os"; +"github.com/docker/docker-credential-helpers/client" -> "os/exec"; +"github.com/docker/docker-credential-helpers/client" -> "strings"; +"github.com/docker/docker-credential-helpers/credentials" [label="github.com/docker/docker-credential-helpers/credentials" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" target="_blank"]; +"github.com/docker/docker-credential-helpers/credentials" -> "bufio"; +"github.com/docker/docker-credential-helpers/credentials" -> "bytes"; +"github.com/docker/docker-credential-helpers/credentials" -> "encoding/json"; +"github.com/docker/docker-credential-helpers/credentials" -> "fmt"; +"github.com/docker/docker-credential-helpers/credentials" -> "io"; +"github.com/docker/docker-credential-helpers/credentials" -> "os"; +"github.com/docker/docker-credential-helpers/credentials" -> "strings"; +"github.com/docker/docker/api/types/versions" [label="github.com/docker/docker/api/types/versions" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/api/types/versions" target="_blank"]; +"github.com/docker/docker/api/types/versions" -> "strconv"; +"github.com/docker/docker/api/types/versions" -> "strings"; +"github.com/docker/docker/pkg/homedir" [label="github.com/docker/docker/pkg/homedir" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/homedir" target="_blank"]; +"github.com/docker/docker/pkg/homedir" -> "github.com/docker/docker/pkg/idtools"; +"github.com/docker/docker/pkg/homedir" -> "github.com/opencontainers/runc/libcontainer/user"; +"github.com/docker/docker/pkg/homedir" -> "os"; +"github.com/docker/docker/pkg/idtools" [label="github.com/docker/docker/pkg/idtools" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/idtools" target="_blank"]; +"github.com/docker/docker/pkg/idtools" -> "bufio"; +"github.com/docker/docker/pkg/idtools" -> "bytes"; +"github.com/docker/docker/pkg/idtools" -> "fmt"; +"github.com/docker/docker/pkg/idtools" -> "github.com/docker/docker/pkg/system"; +"github.com/docker/docker/pkg/idtools" -> "github.com/opencontainers/runc/libcontainer/user"; +"github.com/docker/docker/pkg/idtools" -> "io"; +"github.com/docker/docker/pkg/idtools" -> "os"; +"github.com/docker/docker/pkg/idtools" -> "os/exec"; +"github.com/docker/docker/pkg/idtools" -> "path/filepath"; +"github.com/docker/docker/pkg/idtools" -> "regexp"; +"github.com/docker/docker/pkg/idtools" -> "sort"; +"github.com/docker/docker/pkg/idtools" -> "strconv"; +"github.com/docker/docker/pkg/idtools" -> "strings"; +"github.com/docker/docker/pkg/idtools" -> "sync"; +"github.com/docker/docker/pkg/idtools" -> "syscall"; +"github.com/docker/docker/pkg/mount" [label="github.com/docker/docker/pkg/mount" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/mount" target="_blank"]; +"github.com/docker/docker/pkg/mount" -> "bufio"; +"github.com/docker/docker/pkg/mount" -> "fmt"; +"github.com/docker/docker/pkg/mount" -> "github.com/pkg/errors"; +"github.com/docker/docker/pkg/mount" -> "github.com/sirupsen/logrus"; +"github.com/docker/docker/pkg/mount" -> "golang.org/x/sys/unix"; +"github.com/docker/docker/pkg/mount" -> "io"; +"github.com/docker/docker/pkg/mount" -> "os"; +"github.com/docker/docker/pkg/mount" -> "sort"; +"github.com/docker/docker/pkg/mount" -> "strconv"; +"github.com/docker/docker/pkg/mount" -> "strings"; +"github.com/docker/docker/pkg/system" [label="github.com/docker/docker/pkg/system" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/system" target="_blank"]; +"github.com/docker/docker/pkg/system" -> "bufio"; +"github.com/docker/docker/pkg/system" -> "errors"; +"github.com/docker/docker/pkg/system" -> "fmt"; +"github.com/docker/docker/pkg/system" -> "github.com/docker/docker/pkg/mount"; +"github.com/docker/docker/pkg/system" -> "github.com/docker/go-units"; +"github.com/docker/docker/pkg/system" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/docker/docker/pkg/system" -> "github.com/pkg/errors"; +"github.com/docker/docker/pkg/system" -> "golang.org/x/sys/unix"; +"github.com/docker/docker/pkg/system" -> "io"; +"github.com/docker/docker/pkg/system" -> "io/ioutil"; +"github.com/docker/docker/pkg/system" -> "os"; +"github.com/docker/docker/pkg/system" -> "os/exec"; +"github.com/docker/docker/pkg/system" -> "path/filepath"; +"github.com/docker/docker/pkg/system" -> "runtime"; +"github.com/docker/docker/pkg/system" -> "strconv"; +"github.com/docker/docker/pkg/system" -> "strings"; +"github.com/docker/docker/pkg/system" -> "syscall"; +"github.com/docker/docker/pkg/system" -> "time"; +"github.com/docker/docker/pkg/system" -> "unsafe"; +"github.com/docker/go-connections/sockets" [label="github.com/docker/go-connections/sockets" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-connections/sockets" target="_blank"]; +"github.com/docker/go-connections/sockets" -> "crypto/tls"; +"github.com/docker/go-connections/sockets" -> "errors"; +"github.com/docker/go-connections/sockets" -> "fmt"; +"github.com/docker/go-connections/sockets" -> "golang.org/x/net/proxy"; +"github.com/docker/go-connections/sockets" -> "net"; +"github.com/docker/go-connections/sockets" -> "net/http"; +"github.com/docker/go-connections/sockets" -> "net/url"; +"github.com/docker/go-connections/sockets" -> "os"; +"github.com/docker/go-connections/sockets" -> "strings"; +"github.com/docker/go-connections/sockets" -> "sync"; +"github.com/docker/go-connections/sockets" -> "syscall"; +"github.com/docker/go-connections/sockets" -> "time"; +"github.com/docker/go-connections/tlsconfig" [label="github.com/docker/go-connections/tlsconfig" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-connections/tlsconfig" target="_blank"]; +"github.com/docker/go-connections/tlsconfig" -> "crypto/tls"; +"github.com/docker/go-connections/tlsconfig" -> "crypto/x509"; +"github.com/docker/go-connections/tlsconfig" -> "encoding/pem"; +"github.com/docker/go-connections/tlsconfig" -> "fmt"; +"github.com/docker/go-connections/tlsconfig" -> "github.com/pkg/errors"; +"github.com/docker/go-connections/tlsconfig" -> "io/ioutil"; +"github.com/docker/go-connections/tlsconfig" -> "os"; +"github.com/docker/go-connections/tlsconfig" -> "runtime"; +"github.com/docker/go-metrics" [label="github.com/docker/go-metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-metrics" target="_blank"]; +"github.com/docker/go-metrics" -> "fmt"; +"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus"; +"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus/promhttp"; +"github.com/docker/go-metrics" -> "net/http"; +"github.com/docker/go-metrics" -> "sync"; +"github.com/docker/go-metrics" -> "time"; +"github.com/docker/go-units" [label="github.com/docker/go-units" color="palegoldenrod" URL="https://godoc.org/github.com/docker/go-units" target="_blank"]; +"github.com/docker/go-units" -> "fmt"; +"github.com/docker/go-units" -> "regexp"; +"github.com/docker/go-units" -> "strconv"; +"github.com/docker/go-units" -> "strings"; +"github.com/docker/go-units" -> "time"; +"github.com/ghodss/yaml" [label="github.com/ghodss/yaml" color="paleturquoise" URL="https://godoc.org/github.com/ghodss/yaml" target="_blank"]; +"github.com/ghodss/yaml" -> "bytes"; +"github.com/ghodss/yaml" -> "encoding"; +"github.com/ghodss/yaml" -> "encoding/json"; +"github.com/ghodss/yaml" -> "fmt"; +"github.com/ghodss/yaml" -> "gopkg.in/yaml.v2"; +"github.com/ghodss/yaml" -> "reflect"; +"github.com/ghodss/yaml" -> "sort"; +"github.com/ghodss/yaml" -> "strconv"; +"github.com/ghodss/yaml" -> "strings"; +"github.com/ghodss/yaml" -> "sync"; +"github.com/ghodss/yaml" -> "unicode"; +"github.com/ghodss/yaml" -> "unicode/utf8"; +"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"]; +"github.com/golang/protobuf/proto" -> "bufio"; +"github.com/golang/protobuf/proto" -> "bytes"; +"github.com/golang/protobuf/proto" -> "encoding"; +"github.com/golang/protobuf/proto" -> "encoding/json"; +"github.com/golang/protobuf/proto" -> "errors"; +"github.com/golang/protobuf/proto" -> "fmt"; +"github.com/golang/protobuf/proto" -> "io"; +"github.com/golang/protobuf/proto" -> "log"; +"github.com/golang/protobuf/proto" -> "math"; +"github.com/golang/protobuf/proto" -> "reflect"; +"github.com/golang/protobuf/proto" -> "sort"; +"github.com/golang/protobuf/proto" -> "strconv"; +"github.com/golang/protobuf/proto" -> "strings"; +"github.com/golang/protobuf/proto" -> "sync"; +"github.com/golang/protobuf/proto" -> "sync/atomic"; +"github.com/golang/protobuf/proto" -> "unicode/utf8"; +"github.com/golang/protobuf/proto" -> "unsafe"; +"github.com/gorilla/mux" [label="github.com/gorilla/mux" color="paleturquoise" URL="https://godoc.org/github.com/gorilla/mux" target="_blank"]; +"github.com/gorilla/mux" -> "bytes"; +"github.com/gorilla/mux" -> "context"; +"github.com/gorilla/mux" -> "errors"; +"github.com/gorilla/mux" -> "fmt"; +"github.com/gorilla/mux" -> "net/http"; +"github.com/gorilla/mux" -> "net/url"; +"github.com/gorilla/mux" -> "path"; +"github.com/gorilla/mux" -> "regexp"; +"github.com/gorilla/mux" -> "strconv"; +"github.com/gorilla/mux" -> "strings"; +"github.com/klauspost/compress/flate" [label="github.com/klauspost/compress/flate" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/flate" target="_blank"]; +"github.com/klauspost/compress/flate" -> "bufio"; +"github.com/klauspost/compress/flate" -> "bytes"; +"github.com/klauspost/compress/flate" -> "encoding/binary"; +"github.com/klauspost/compress/flate" -> "fmt"; +"github.com/klauspost/compress/flate" -> "io"; +"github.com/klauspost/compress/flate" -> "math"; +"github.com/klauspost/compress/flate" -> "math/bits"; +"github.com/klauspost/compress/flate" -> "sort"; +"github.com/klauspost/compress/flate" -> "strconv"; +"github.com/klauspost/compress/flate" -> "sync"; +"github.com/klauspost/compress/fse" [label="github.com/klauspost/compress/fse" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/fse" target="_blank"]; +"github.com/klauspost/compress/fse" -> "errors"; +"github.com/klauspost/compress/fse" -> "fmt"; +"github.com/klauspost/compress/fse" -> "io"; +"github.com/klauspost/compress/fse" -> "math/bits"; +"github.com/klauspost/compress/huff0" [label="github.com/klauspost/compress/huff0" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/huff0" target="_blank"]; +"github.com/klauspost/compress/huff0" -> "errors"; +"github.com/klauspost/compress/huff0" -> "fmt"; +"github.com/klauspost/compress/huff0" -> "github.com/klauspost/compress/fse"; +"github.com/klauspost/compress/huff0" -> "io"; +"github.com/klauspost/compress/huff0" -> "math"; +"github.com/klauspost/compress/huff0" -> "math/bits"; +"github.com/klauspost/compress/huff0" -> "runtime"; +"github.com/klauspost/compress/huff0" -> "sync"; +"github.com/klauspost/compress/snappy" [label="github.com/klauspost/compress/snappy" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/snappy" target="_blank"]; +"github.com/klauspost/compress/snappy" -> "encoding/binary"; +"github.com/klauspost/compress/snappy" -> "errors"; +"github.com/klauspost/compress/snappy" -> "hash/crc32"; +"github.com/klauspost/compress/snappy" -> "io"; +"github.com/klauspost/compress/zstd" [label="github.com/klauspost/compress/zstd" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/zstd" target="_blank"]; +"github.com/klauspost/compress/zstd" -> "bytes"; +"github.com/klauspost/compress/zstd" -> "crypto/rand"; +"github.com/klauspost/compress/zstd" -> "encoding/binary"; +"github.com/klauspost/compress/zstd" -> "encoding/hex"; +"github.com/klauspost/compress/zstd" -> "errors"; +"github.com/klauspost/compress/zstd" -> "fmt"; +"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/huff0"; +"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/snappy"; +"github.com/klauspost/compress/zstd" -> "github.com/klauspost/compress/zstd/internal/xxhash"; +"github.com/klauspost/compress/zstd" -> "hash"; +"github.com/klauspost/compress/zstd" -> "hash/crc32"; +"github.com/klauspost/compress/zstd" -> "io"; +"github.com/klauspost/compress/zstd" -> "io/ioutil"; +"github.com/klauspost/compress/zstd" -> "log"; +"github.com/klauspost/compress/zstd" -> "math"; +"github.com/klauspost/compress/zstd" -> "math/bits"; +"github.com/klauspost/compress/zstd" -> "runtime"; +"github.com/klauspost/compress/zstd" -> "runtime/debug"; +"github.com/klauspost/compress/zstd" -> "strconv"; +"github.com/klauspost/compress/zstd" -> "strings"; +"github.com/klauspost/compress/zstd" -> "sync"; +"github.com/klauspost/compress/zstd/internal/xxhash" [label="github.com/klauspost/compress/zstd/internal/xxhash" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/compress/zstd/internal/xxhash" target="_blank"]; +"github.com/klauspost/compress/zstd/internal/xxhash" -> "encoding/binary"; +"github.com/klauspost/compress/zstd/internal/xxhash" -> "errors"; +"github.com/klauspost/compress/zstd/internal/xxhash" -> "math/bits"; +"github.com/klauspost/pgzip" [label="github.com/klauspost/pgzip" color="paleturquoise" URL="https://godoc.org/github.com/klauspost/pgzip" target="_blank"]; +"github.com/klauspost/pgzip" -> "bufio"; +"github.com/klauspost/pgzip" -> "bytes"; +"github.com/klauspost/pgzip" -> "errors"; +"github.com/klauspost/pgzip" -> "fmt"; +"github.com/klauspost/pgzip" -> "github.com/klauspost/compress/flate"; +"github.com/klauspost/pgzip" -> "hash"; +"github.com/klauspost/pgzip" -> "hash/crc32"; +"github.com/klauspost/pgzip" -> "io"; +"github.com/klauspost/pgzip" -> "sync"; +"github.com/klauspost/pgzip" -> "time"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" [label="github.com/matttproud/golang_protobuf_extensions/pbutil" color="paleturquoise" URL="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"]; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "encoding/binary"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "errors"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "github.com/golang/protobuf/proto"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "io"; +"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"]; +"github.com/opencontainers/go-digest" -> "crypto"; +"github.com/opencontainers/go-digest" -> "fmt"; +"github.com/opencontainers/go-digest" -> "hash"; +"github.com/opencontainers/go-digest" -> "io"; +"github.com/opencontainers/go-digest" -> "regexp"; +"github.com/opencontainers/go-digest" -> "strings"; +"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go" -> "fmt"; +"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "time"; +"github.com/opencontainers/runc/libcontainer/user" [label="github.com/opencontainers/runc/libcontainer/user" color="palegoldenrod" URL="https://godoc.org/github.com/opencontainers/runc/libcontainer/user" target="_blank"]; +"github.com/opencontainers/runc/libcontainer/user" -> "bufio"; +"github.com/opencontainers/runc/libcontainer/user" -> "errors"; +"github.com/opencontainers/runc/libcontainer/user" -> "fmt"; +"github.com/opencontainers/runc/libcontainer/user" -> "golang.org/x/sys/unix"; +"github.com/opencontainers/runc/libcontainer/user" -> "io"; +"github.com/opencontainers/runc/libcontainer/user" -> "os"; +"github.com/opencontainers/runc/libcontainer/user" -> "os/user"; +"github.com/opencontainers/runc/libcontainer/user" -> "strconv"; +"github.com/opencontainers/runc/libcontainer/user" -> "strings"; +"github.com/pkg/errors" [label="github.com/pkg/errors" color="paleturquoise" URL="https://godoc.org/github.com/pkg/errors" target="_blank"]; +"github.com/pkg/errors" -> "fmt"; +"github.com/pkg/errors" -> "io"; +"github.com/pkg/errors" -> "path"; +"github.com/pkg/errors" -> "runtime"; +"github.com/pkg/errors" -> "strings"; +"github.com/prometheus/client_golang/prometheus" [label="github.com/prometheus/client_golang/prometheus" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus" target="_blank"]; +"github.com/prometheus/client_golang/prometheus" -> "bytes"; +"github.com/prometheus/client_golang/prometheus" -> "encoding/json"; +"github.com/prometheus/client_golang/prometheus" -> "errors"; +"github.com/prometheus/client_golang/prometheus" -> "expvar"; +"github.com/prometheus/client_golang/prometheus" -> "fmt"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/beorn7/perks/quantile"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/cespare/xxhash/v2"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_golang/prometheus/internal"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/expfmt"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/model"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/procfs"; +"github.com/prometheus/client_golang/prometheus" -> "io/ioutil"; +"github.com/prometheus/client_golang/prometheus" -> "math"; +"github.com/prometheus/client_golang/prometheus" -> "os"; +"github.com/prometheus/client_golang/prometheus" -> "path/filepath"; +"github.com/prometheus/client_golang/prometheus" -> "runtime"; +"github.com/prometheus/client_golang/prometheus" -> "runtime/debug"; +"github.com/prometheus/client_golang/prometheus" -> "sort"; +"github.com/prometheus/client_golang/prometheus" -> "strings"; +"github.com/prometheus/client_golang/prometheus" -> "sync"; +"github.com/prometheus/client_golang/prometheus" -> "sync/atomic"; +"github.com/prometheus/client_golang/prometheus" -> "time"; +"github.com/prometheus/client_golang/prometheus" -> "unicode/utf8"; +"github.com/prometheus/client_golang/prometheus/internal" [label="github.com/prometheus/client_golang/prometheus/internal" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" target="_blank"]; +"github.com/prometheus/client_golang/prometheus/internal" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus/internal" -> "sort"; +"github.com/prometheus/client_golang/prometheus/promhttp" [label="github.com/prometheus/client_golang/prometheus/promhttp" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"]; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "bufio"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "compress/gzip"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "crypto/tls"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "errors"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "fmt"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_golang/prometheus"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/common/expfmt"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "io"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http/httptrace"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "strconv"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "strings"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "sync"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "time"; +"github.com/prometheus/client_model/go" [label="github.com/prometheus/client_model/go" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_model/go" target="_blank"]; +"github.com/prometheus/client_model/go" -> "fmt"; +"github.com/prometheus/client_model/go" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/client_model/go" -> "math"; +"github.com/prometheus/common/expfmt" [label="github.com/prometheus/common/expfmt" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/expfmt" target="_blank"]; +"github.com/prometheus/common/expfmt" -> "bufio"; +"github.com/prometheus/common/expfmt" -> "bytes"; +"github.com/prometheus/common/expfmt" -> "fmt"; +"github.com/prometheus/common/expfmt" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/common/expfmt" -> "github.com/matttproud/golang_protobuf_extensions/pbutil"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/model"; +"github.com/prometheus/common/expfmt" -> "io"; +"github.com/prometheus/common/expfmt" -> "io/ioutil"; +"github.com/prometheus/common/expfmt" -> "math"; +"github.com/prometheus/common/expfmt" -> "mime"; +"github.com/prometheus/common/expfmt" -> "net/http"; +"github.com/prometheus/common/expfmt" -> "strconv"; +"github.com/prometheus/common/expfmt" -> "strings"; +"github.com/prometheus/common/expfmt" -> "sync"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" [label="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"]; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "sort"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strconv"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strings"; +"github.com/prometheus/common/model" [label="github.com/prometheus/common/model" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/model" target="_blank"]; +"github.com/prometheus/common/model" -> "encoding/json"; +"github.com/prometheus/common/model" -> "fmt"; +"github.com/prometheus/common/model" -> "math"; +"github.com/prometheus/common/model" -> "regexp"; +"github.com/prometheus/common/model" -> "sort"; +"github.com/prometheus/common/model" -> "strconv"; +"github.com/prometheus/common/model" -> "strings"; +"github.com/prometheus/common/model" -> "time"; +"github.com/prometheus/common/model" -> "unicode/utf8"; +"github.com/prometheus/procfs" [label="github.com/prometheus/procfs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs" target="_blank"]; +"github.com/prometheus/procfs" -> "bufio"; +"github.com/prometheus/procfs" -> "bytes"; +"github.com/prometheus/procfs" -> "encoding/hex"; +"github.com/prometheus/procfs" -> "errors"; +"github.com/prometheus/procfs" -> "fmt"; +"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/fs"; +"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/util"; +"github.com/prometheus/procfs" -> "io"; +"github.com/prometheus/procfs" -> "io/ioutil"; +"github.com/prometheus/procfs" -> "net"; +"github.com/prometheus/procfs" -> "os"; +"github.com/prometheus/procfs" -> "path/filepath"; +"github.com/prometheus/procfs" -> "regexp"; +"github.com/prometheus/procfs" -> "sort"; +"github.com/prometheus/procfs" -> "strconv"; +"github.com/prometheus/procfs" -> "strings"; +"github.com/prometheus/procfs" -> "time"; +"github.com/prometheus/procfs/internal/fs" [label="github.com/prometheus/procfs/internal/fs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/fs" target="_blank"]; +"github.com/prometheus/procfs/internal/fs" -> "fmt"; +"github.com/prometheus/procfs/internal/fs" -> "os"; +"github.com/prometheus/procfs/internal/fs" -> "path/filepath"; +"github.com/prometheus/procfs/internal/util" [label="github.com/prometheus/procfs/internal/util" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/util" target="_blank"]; +"github.com/prometheus/procfs/internal/util" -> "bytes"; +"github.com/prometheus/procfs/internal/util" -> "io/ioutil"; +"github.com/prometheus/procfs/internal/util" -> "os"; +"github.com/prometheus/procfs/internal/util" -> "strconv"; +"github.com/prometheus/procfs/internal/util" -> "strings"; +"github.com/prometheus/procfs/internal/util" -> "syscall"; +"github.com/sirupsen/logrus" [label="github.com/sirupsen/logrus" color="paleturquoise" URL="https://godoc.org/github.com/sirupsen/logrus" target="_blank"]; +"github.com/sirupsen/logrus" -> "bufio"; +"github.com/sirupsen/logrus" -> "bytes"; +"github.com/sirupsen/logrus" -> "context"; +"github.com/sirupsen/logrus" -> "encoding/json"; +"github.com/sirupsen/logrus" -> "fmt"; +"github.com/sirupsen/logrus" -> "golang.org/x/sys/unix"; +"github.com/sirupsen/logrus" -> "io"; +"github.com/sirupsen/logrus" -> "log"; +"github.com/sirupsen/logrus" -> "os"; +"github.com/sirupsen/logrus" -> "reflect"; +"github.com/sirupsen/logrus" -> "runtime"; +"github.com/sirupsen/logrus" -> "sort"; +"github.com/sirupsen/logrus" -> "strings"; +"github.com/sirupsen/logrus" -> "sync"; +"github.com/sirupsen/logrus" -> "sync/atomic"; +"github.com/sirupsen/logrus" -> "time"; +"github.com/ulikunitz/xz" [label="github.com/ulikunitz/xz" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz" target="_blank"]; +"github.com/ulikunitz/xz" -> "bytes"; +"github.com/ulikunitz/xz" -> "crypto/sha256"; +"github.com/ulikunitz/xz" -> "errors"; +"github.com/ulikunitz/xz" -> "fmt"; +"github.com/ulikunitz/xz" -> "github.com/ulikunitz/xz/internal/xlog"; +"github.com/ulikunitz/xz" -> "github.com/ulikunitz/xz/lzma"; +"github.com/ulikunitz/xz" -> "hash"; +"github.com/ulikunitz/xz" -> "hash/crc32"; +"github.com/ulikunitz/xz" -> "hash/crc64"; +"github.com/ulikunitz/xz" -> "io"; +"github.com/ulikunitz/xz/internal/hash" [label="github.com/ulikunitz/xz/internal/hash" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/internal/hash" target="_blank"]; +"github.com/ulikunitz/xz/internal/xlog" [label="github.com/ulikunitz/xz/internal/xlog" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/internal/xlog" target="_blank"]; +"github.com/ulikunitz/xz/internal/xlog" -> "fmt"; +"github.com/ulikunitz/xz/internal/xlog" -> "io"; +"github.com/ulikunitz/xz/internal/xlog" -> "os"; +"github.com/ulikunitz/xz/internal/xlog" -> "runtime"; +"github.com/ulikunitz/xz/internal/xlog" -> "sync"; +"github.com/ulikunitz/xz/internal/xlog" -> "time"; +"github.com/ulikunitz/xz/lzma" [label="github.com/ulikunitz/xz/lzma" color="paleturquoise" URL="https://godoc.org/github.com/ulikunitz/xz/lzma" target="_blank"]; +"github.com/ulikunitz/xz/lzma" -> "bufio"; +"github.com/ulikunitz/xz/lzma" -> "bytes"; +"github.com/ulikunitz/xz/lzma" -> "errors"; +"github.com/ulikunitz/xz/lzma" -> "fmt"; +"github.com/ulikunitz/xz/lzma" -> "github.com/ulikunitz/xz/internal/hash"; +"github.com/ulikunitz/xz/lzma" -> "github.com/ulikunitz/xz/internal/xlog"; +"github.com/ulikunitz/xz/lzma" -> "io"; +"github.com/ulikunitz/xz/lzma" -> "unicode"; +"golang.org/x/net/internal/socks" [label="golang.org/x/net/internal/socks" color="paleturquoise" URL="https://godoc.org/golang.org/x/net/internal/socks" target="_blank"]; +"golang.org/x/net/internal/socks" -> "context"; +"golang.org/x/net/internal/socks" -> "errors"; +"golang.org/x/net/internal/socks" -> "io"; +"golang.org/x/net/internal/socks" -> "net"; +"golang.org/x/net/internal/socks" -> "strconv"; +"golang.org/x/net/internal/socks" -> "time"; +"golang.org/x/net/proxy" [label="golang.org/x/net/proxy" color="paleturquoise" URL="https://godoc.org/golang.org/x/net/proxy" target="_blank"]; +"golang.org/x/net/proxy" -> "context"; +"golang.org/x/net/proxy" -> "errors"; +"golang.org/x/net/proxy" -> "golang.org/x/net/internal/socks"; +"golang.org/x/net/proxy" -> "net"; +"golang.org/x/net/proxy" -> "net/url"; +"golang.org/x/net/proxy" -> "os"; +"golang.org/x/net/proxy" -> "strings"; +"golang.org/x/net/proxy" -> "sync"; +"golang.org/x/sys/unix" [label="golang.org/x/sys/unix" color="paleturquoise" URL="https://godoc.org/golang.org/x/sys/unix" target="_blank"]; +"golang.org/x/sys/unix" -> "bytes"; +"golang.org/x/sys/unix" -> "encoding/binary"; +"golang.org/x/sys/unix" -> "net"; +"golang.org/x/sys/unix" -> "runtime"; +"golang.org/x/sys/unix" -> "sort"; +"golang.org/x/sys/unix" -> "strings"; +"golang.org/x/sys/unix" -> "sync"; +"golang.org/x/sys/unix" -> "syscall"; +"golang.org/x/sys/unix" -> "time"; +"golang.org/x/sys/unix" -> "unsafe"; +"gopkg.in/yaml.v2" [label="gopkg.in/yaml.v2" color="paleturquoise" URL="https://godoc.org/gopkg.in/yaml.v2" target="_blank"]; +"gopkg.in/yaml.v2" -> "bytes"; +"gopkg.in/yaml.v2" -> "encoding"; +"gopkg.in/yaml.v2" -> "encoding/base64"; +"gopkg.in/yaml.v2" -> "errors"; +"gopkg.in/yaml.v2" -> "fmt"; +"gopkg.in/yaml.v2" -> "io"; +"gopkg.in/yaml.v2" -> "math"; +"gopkg.in/yaml.v2" -> "reflect"; +"gopkg.in/yaml.v2" -> "regexp"; +"gopkg.in/yaml.v2" -> "sort"; +"gopkg.in/yaml.v2" -> "strconv"; +"gopkg.in/yaml.v2" -> "strings"; +"gopkg.in/yaml.v2" -> "sync"; +"gopkg.in/yaml.v2" -> "time"; +"gopkg.in/yaml.v2" -> "unicode"; +"gopkg.in/yaml.v2" -> "unicode/utf8"; +"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"]; +"hash/crc32" [label="hash/crc32" color="palegreen" URL="https://godoc.org/hash/crc32" target="_blank"]; +"hash/crc64" [label="hash/crc64" color="palegreen" URL="https://godoc.org/hash/crc64" target="_blank"]; +"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"]; +"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"]; +"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"]; +"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"]; +"math/big" [label="math/big" color="palegreen" URL="https://godoc.org/math/big" target="_blank"]; +"math/bits" [label="math/bits" color="palegreen" URL="https://godoc.org/math/bits" target="_blank"]; +"mime" [label="mime" color="palegreen" URL="https://godoc.org/mime" target="_blank"]; +"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"]; +"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"]; +"net/http/httptrace" [label="net/http/httptrace" color="palegreen" URL="https://godoc.org/net/http/httptrace" target="_blank"]; +"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"]; +"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"]; +"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"]; +"os/user" [label="os/user" color="palegreen" URL="https://godoc.org/os/user" target="_blank"]; +"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"]; +"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"]; +"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"]; +"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"]; +"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"]; +"runtime/debug" [label="runtime/debug" color="palegreen" URL="https://godoc.org/runtime/debug" target="_blank"]; +"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"]; +"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"]; +"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"]; +"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"]; +"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"]; +"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"]; +"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"]; +"unicode" [label="unicode" color="palegreen" URL="https://godoc.org/unicode" target="_blank"]; +"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"]; +"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"]; +} diff --git a/images/dot/docker.dot b/images/dot/docker.dot new file mode 100644 index 0000000..90dc677 --- /dev/null +++ b/images/dot/docker.dot @@ -0,0 +1,327 @@ +digraph godep { +nodesep=0.4 +ranksep=0.8 +node [shape="box",style="rounded,filled"] +edge [arrowsize="0.5"] +"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"]; +"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"]; +"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"]; +"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"]; +"crypto" [label="crypto" color="palegreen" URL="https://godoc.org/crypto" target="_blank"]; +"crypto/tls" [label="crypto/tls" color="palegreen" URL="https://godoc.org/crypto/tls" target="_blank"]; +"encoding" [label="encoding" color="palegreen" URL="https://godoc.org/encoding" target="_blank"]; +"encoding/binary" [label="encoding/binary" color="palegreen" URL="https://godoc.org/encoding/binary" target="_blank"]; +"encoding/hex" [label="encoding/hex" color="palegreen" URL="https://godoc.org/encoding/hex" target="_blank"]; +"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"]; +"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"]; +"expvar" [label="expvar" color="palegreen" URL="https://godoc.org/expvar" target="_blank"]; +"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"]; +"github.com/beorn7/perks/quantile" [label="github.com/beorn7/perks/quantile" color="paleturquoise" URL="https://godoc.org/github.com/beorn7/perks/quantile" target="_blank"]; +"github.com/beorn7/perks/quantile" -> "math"; +"github.com/beorn7/perks/quantile" -> "sort"; +"github.com/cespare/xxhash/v2" [label="github.com/cespare/xxhash/v2" color="paleturquoise" URL="https://godoc.org/github.com/cespare/xxhash/v2" target="_blank"]; +"github.com/cespare/xxhash/v2" -> "encoding/binary"; +"github.com/cespare/xxhash/v2" -> "errors"; +"github.com/cespare/xxhash/v2" -> "math/bits"; +"github.com/cespare/xxhash/v2" -> "reflect"; +"github.com/cespare/xxhash/v2" -> "unsafe"; +"github.com/docker/distribution" [label="github.com/docker/distribution" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution" target="_blank"]; +"github.com/docker/distribution" -> "context"; +"github.com/docker/distribution" -> "errors"; +"github.com/docker/distribution" -> "fmt"; +"github.com/docker/distribution" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution" -> "github.com/opencontainers/image-spec/specs-go/v1"; +"github.com/docker/distribution" -> "io"; +"github.com/docker/distribution" -> "mime"; +"github.com/docker/distribution" -> "net/http"; +"github.com/docker/distribution" -> "strings"; +"github.com/docker/distribution" -> "time"; +"github.com/docker/distribution/digestset" [label="github.com/docker/distribution/digestset" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/digestset" target="_blank"]; +"github.com/docker/distribution/digestset" -> "errors"; +"github.com/docker/distribution/digestset" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/digestset" -> "sort"; +"github.com/docker/distribution/digestset" -> "strings"; +"github.com/docker/distribution/digestset" -> "sync"; +"github.com/docker/distribution/metrics" [label="github.com/docker/distribution/metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/metrics" target="_blank"]; +"github.com/docker/distribution/metrics" -> "github.com/docker/go-metrics"; +"github.com/docker/distribution/reference" [label="github.com/docker/distribution/reference" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/reference" target="_blank"]; +"github.com/docker/distribution/reference" -> "errors"; +"github.com/docker/distribution/reference" -> "fmt"; +"github.com/docker/distribution/reference" -> "github.com/docker/distribution/digestset"; +"github.com/docker/distribution/reference" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/reference" -> "path"; +"github.com/docker/distribution/reference" -> "regexp"; +"github.com/docker/distribution/reference" -> "strings"; +"github.com/docker/distribution/registry/api/errcode" [label="github.com/docker/distribution/registry/api/errcode" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/errcode" target="_blank"]; +"github.com/docker/distribution/registry/api/errcode" -> "encoding/json"; +"github.com/docker/distribution/registry/api/errcode" -> "fmt"; +"github.com/docker/distribution/registry/api/errcode" -> "net/http"; +"github.com/docker/distribution/registry/api/errcode" -> "sort"; +"github.com/docker/distribution/registry/api/errcode" -> "strings"; +"github.com/docker/distribution/registry/api/errcode" -> "sync"; +"github.com/docker/distribution/registry/api/v2" [label="github.com/docker/distribution/registry/api/v2" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/api/v2" target="_blank"]; +"github.com/docker/distribution/registry/api/v2" -> "fmt"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/gorilla/mux"; +"github.com/docker/distribution/registry/api/v2" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/api/v2" -> "net/http"; +"github.com/docker/distribution/registry/api/v2" -> "net/url"; +"github.com/docker/distribution/registry/api/v2" -> "regexp"; +"github.com/docker/distribution/registry/api/v2" -> "strings"; +"github.com/docker/distribution/registry/api/v2" -> "unicode"; +"github.com/docker/distribution/registry/client" [label="github.com/docker/distribution/registry/client" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client" target="_blank"]; +"github.com/docker/distribution/registry/client" -> "bytes"; +"github.com/docker/distribution/registry/client" -> "context"; +"github.com/docker/distribution/registry/client" -> "encoding/json"; +"github.com/docker/distribution/registry/client" -> "errors"; +"github.com/docker/distribution/registry/client" -> "fmt"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/errcode"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/api/v2"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/auth/challenge"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/client/transport"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache"; +"github.com/docker/distribution/registry/client" -> "github.com/docker/distribution/registry/storage/cache/memory"; +"github.com/docker/distribution/registry/client" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/client" -> "io"; +"github.com/docker/distribution/registry/client" -> "io/ioutil"; +"github.com/docker/distribution/registry/client" -> "net/http"; +"github.com/docker/distribution/registry/client" -> "net/url"; +"github.com/docker/distribution/registry/client" -> "strconv"; +"github.com/docker/distribution/registry/client" -> "strings"; +"github.com/docker/distribution/registry/client" -> "time"; +"github.com/docker/distribution/registry/client/auth" [label="github.com/docker/distribution/registry/client/auth" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth" target="_blank"]; +"github.com/docker/distribution/registry/client/auth" -> "encoding/json"; +"github.com/docker/distribution/registry/client/auth" -> "errors"; +"github.com/docker/distribution/registry/client/auth" -> "fmt"; +"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client"; +"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client/auth/challenge"; +"github.com/docker/distribution/registry/client/auth" -> "github.com/docker/distribution/registry/client/transport"; +"github.com/docker/distribution/registry/client/auth" -> "net/http"; +"github.com/docker/distribution/registry/client/auth" -> "net/url"; +"github.com/docker/distribution/registry/client/auth" -> "strings"; +"github.com/docker/distribution/registry/client/auth" -> "sync"; +"github.com/docker/distribution/registry/client/auth" -> "time"; +"github.com/docker/distribution/registry/client/auth/challenge" [label="github.com/docker/distribution/registry/client/auth/challenge" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/auth/challenge" target="_blank"]; +"github.com/docker/distribution/registry/client/auth/challenge" -> "fmt"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "net/http"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "net/url"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "strings"; +"github.com/docker/distribution/registry/client/auth/challenge" -> "sync"; +"github.com/docker/distribution/registry/client/transport" [label="github.com/docker/distribution/registry/client/transport" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/client/transport" target="_blank"]; +"github.com/docker/distribution/registry/client/transport" -> "errors"; +"github.com/docker/distribution/registry/client/transport" -> "fmt"; +"github.com/docker/distribution/registry/client/transport" -> "io"; +"github.com/docker/distribution/registry/client/transport" -> "net/http"; +"github.com/docker/distribution/registry/client/transport" -> "regexp"; +"github.com/docker/distribution/registry/client/transport" -> "strconv"; +"github.com/docker/distribution/registry/client/transport" -> "sync"; +"github.com/docker/distribution/registry/storage/cache" [label="github.com/docker/distribution/registry/storage/cache" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache" target="_blank"]; +"github.com/docker/distribution/registry/storage/cache" -> "context"; +"github.com/docker/distribution/registry/storage/cache" -> "fmt"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/docker/distribution/metrics"; +"github.com/docker/distribution/registry/storage/cache" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/storage/cache/memory" [label="github.com/docker/distribution/registry/storage/cache/memory" color="paleturquoise" URL="https://godoc.org/github.com/docker/distribution/registry/storage/cache/memory" target="_blank"]; +"github.com/docker/distribution/registry/storage/cache/memory" -> "context"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/reference"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/docker/distribution/registry/storage/cache"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "github.com/opencontainers/go-digest"; +"github.com/docker/distribution/registry/storage/cache/memory" -> "sync"; +"github.com/docker/go-metrics" [label="github.com/docker/go-metrics" color="paleturquoise" URL="https://godoc.org/github.com/docker/go-metrics" target="_blank"]; +"github.com/docker/go-metrics" -> "fmt"; +"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus"; +"github.com/docker/go-metrics" -> "github.com/prometheus/client_golang/prometheus/promhttp"; +"github.com/docker/go-metrics" -> "net/http"; +"github.com/docker/go-metrics" -> "sync"; +"github.com/docker/go-metrics" -> "time"; +"github.com/golang/protobuf/proto" [label="github.com/golang/protobuf/proto" color="paleturquoise" URL="https://godoc.org/github.com/golang/protobuf/proto" target="_blank"]; +"github.com/golang/protobuf/proto" -> "bufio"; +"github.com/golang/protobuf/proto" -> "bytes"; +"github.com/golang/protobuf/proto" -> "encoding"; +"github.com/golang/protobuf/proto" -> "encoding/json"; +"github.com/golang/protobuf/proto" -> "errors"; +"github.com/golang/protobuf/proto" -> "fmt"; +"github.com/golang/protobuf/proto" -> "io"; +"github.com/golang/protobuf/proto" -> "log"; +"github.com/golang/protobuf/proto" -> "math"; +"github.com/golang/protobuf/proto" -> "reflect"; +"github.com/golang/protobuf/proto" -> "sort"; +"github.com/golang/protobuf/proto" -> "strconv"; +"github.com/golang/protobuf/proto" -> "strings"; +"github.com/golang/protobuf/proto" -> "sync"; +"github.com/golang/protobuf/proto" -> "sync/atomic"; +"github.com/golang/protobuf/proto" -> "unicode/utf8"; +"github.com/golang/protobuf/proto" -> "unsafe"; +"github.com/gorilla/mux" [label="github.com/gorilla/mux" color="paleturquoise" URL="https://godoc.org/github.com/gorilla/mux" target="_blank"]; +"github.com/gorilla/mux" -> "bytes"; +"github.com/gorilla/mux" -> "context"; +"github.com/gorilla/mux" -> "errors"; +"github.com/gorilla/mux" -> "fmt"; +"github.com/gorilla/mux" -> "net/http"; +"github.com/gorilla/mux" -> "net/url"; +"github.com/gorilla/mux" -> "path"; +"github.com/gorilla/mux" -> "regexp"; +"github.com/gorilla/mux" -> "strconv"; +"github.com/gorilla/mux" -> "strings"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" [label="github.com/matttproud/golang_protobuf_extensions/pbutil" color="paleturquoise" URL="https://godoc.org/github.com/matttproud/golang_protobuf_extensions/pbutil" target="_blank"]; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "encoding/binary"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "errors"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "github.com/golang/protobuf/proto"; +"github.com/matttproud/golang_protobuf_extensions/pbutil" -> "io"; +"github.com/opencontainers/go-digest" [label="github.com/opencontainers/go-digest" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/go-digest" target="_blank"]; +"github.com/opencontainers/go-digest" -> "crypto"; +"github.com/opencontainers/go-digest" -> "fmt"; +"github.com/opencontainers/go-digest" -> "hash"; +"github.com/opencontainers/go-digest" -> "io"; +"github.com/opencontainers/go-digest" -> "regexp"; +"github.com/opencontainers/go-digest" -> "strings"; +"github.com/opencontainers/image-spec/specs-go" [label="github.com/opencontainers/image-spec/specs-go" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go" -> "fmt"; +"github.com/opencontainers/image-spec/specs-go/v1" [label="github.com/opencontainers/image-spec/specs-go/v1" color="paleturquoise" URL="https://godoc.org/github.com/opencontainers/image-spec/specs-go/v1" target="_blank"]; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/go-digest"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "github.com/opencontainers/image-spec/specs-go"; +"github.com/opencontainers/image-spec/specs-go/v1" -> "time"; +"github.com/prometheus/client_golang/prometheus" [label="github.com/prometheus/client_golang/prometheus" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus" target="_blank"]; +"github.com/prometheus/client_golang/prometheus" -> "bytes"; +"github.com/prometheus/client_golang/prometheus" -> "encoding/json"; +"github.com/prometheus/client_golang/prometheus" -> "errors"; +"github.com/prometheus/client_golang/prometheus" -> "expvar"; +"github.com/prometheus/client_golang/prometheus" -> "fmt"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/beorn7/perks/quantile"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/cespare/xxhash/v2"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_golang/prometheus/internal"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/expfmt"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/common/model"; +"github.com/prometheus/client_golang/prometheus" -> "github.com/prometheus/procfs"; +"github.com/prometheus/client_golang/prometheus" -> "io/ioutil"; +"github.com/prometheus/client_golang/prometheus" -> "math"; +"github.com/prometheus/client_golang/prometheus" -> "os"; +"github.com/prometheus/client_golang/prometheus" -> "path/filepath"; +"github.com/prometheus/client_golang/prometheus" -> "runtime"; +"github.com/prometheus/client_golang/prometheus" -> "runtime/debug"; +"github.com/prometheus/client_golang/prometheus" -> "sort"; +"github.com/prometheus/client_golang/prometheus" -> "strings"; +"github.com/prometheus/client_golang/prometheus" -> "sync"; +"github.com/prometheus/client_golang/prometheus" -> "sync/atomic"; +"github.com/prometheus/client_golang/prometheus" -> "time"; +"github.com/prometheus/client_golang/prometheus" -> "unicode/utf8"; +"github.com/prometheus/client_golang/prometheus/internal" [label="github.com/prometheus/client_golang/prometheus/internal" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/internal" target="_blank"]; +"github.com/prometheus/client_golang/prometheus/internal" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus/internal" -> "sort"; +"github.com/prometheus/client_golang/prometheus/promhttp" [label="github.com/prometheus/client_golang/prometheus/promhttp" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_golang/prometheus/promhttp" target="_blank"]; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "bufio"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "compress/gzip"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "crypto/tls"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "errors"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "fmt"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_golang/prometheus"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "github.com/prometheus/common/expfmt"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "io"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "net/http/httptrace"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "strconv"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "strings"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "sync"; +"github.com/prometheus/client_golang/prometheus/promhttp" -> "time"; +"github.com/prometheus/client_model/go" [label="github.com/prometheus/client_model/go" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/client_model/go" target="_blank"]; +"github.com/prometheus/client_model/go" -> "fmt"; +"github.com/prometheus/client_model/go" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/client_model/go" -> "math"; +"github.com/prometheus/common/expfmt" [label="github.com/prometheus/common/expfmt" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/expfmt" target="_blank"]; +"github.com/prometheus/common/expfmt" -> "bufio"; +"github.com/prometheus/common/expfmt" -> "bytes"; +"github.com/prometheus/common/expfmt" -> "fmt"; +"github.com/prometheus/common/expfmt" -> "github.com/golang/protobuf/proto"; +"github.com/prometheus/common/expfmt" -> "github.com/matttproud/golang_protobuf_extensions/pbutil"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/client_model/go"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg"; +"github.com/prometheus/common/expfmt" -> "github.com/prometheus/common/model"; +"github.com/prometheus/common/expfmt" -> "io"; +"github.com/prometheus/common/expfmt" -> "io/ioutil"; +"github.com/prometheus/common/expfmt" -> "math"; +"github.com/prometheus/common/expfmt" -> "mime"; +"github.com/prometheus/common/expfmt" -> "net/http"; +"github.com/prometheus/common/expfmt" -> "strconv"; +"github.com/prometheus/common/expfmt" -> "strings"; +"github.com/prometheus/common/expfmt" -> "sync"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" [label="github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" target="_blank"]; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "sort"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strconv"; +"github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" -> "strings"; +"github.com/prometheus/common/model" [label="github.com/prometheus/common/model" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/common/model" target="_blank"]; +"github.com/prometheus/common/model" -> "encoding/json"; +"github.com/prometheus/common/model" -> "fmt"; +"github.com/prometheus/common/model" -> "math"; +"github.com/prometheus/common/model" -> "regexp"; +"github.com/prometheus/common/model" -> "sort"; +"github.com/prometheus/common/model" -> "strconv"; +"github.com/prometheus/common/model" -> "strings"; +"github.com/prometheus/common/model" -> "time"; +"github.com/prometheus/common/model" -> "unicode/utf8"; +"github.com/prometheus/procfs" [label="github.com/prometheus/procfs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs" target="_blank"]; +"github.com/prometheus/procfs" -> "bufio"; +"github.com/prometheus/procfs" -> "bytes"; +"github.com/prometheus/procfs" -> "encoding/hex"; +"github.com/prometheus/procfs" -> "errors"; +"github.com/prometheus/procfs" -> "fmt"; +"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/fs"; +"github.com/prometheus/procfs" -> "github.com/prometheus/procfs/internal/util"; +"github.com/prometheus/procfs" -> "io"; +"github.com/prometheus/procfs" -> "io/ioutil"; +"github.com/prometheus/procfs" -> "net"; +"github.com/prometheus/procfs" -> "os"; +"github.com/prometheus/procfs" -> "path/filepath"; +"github.com/prometheus/procfs" -> "regexp"; +"github.com/prometheus/procfs" -> "sort"; +"github.com/prometheus/procfs" -> "strconv"; +"github.com/prometheus/procfs" -> "strings"; +"github.com/prometheus/procfs" -> "time"; +"github.com/prometheus/procfs/internal/fs" [label="github.com/prometheus/procfs/internal/fs" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/fs" target="_blank"]; +"github.com/prometheus/procfs/internal/fs" -> "fmt"; +"github.com/prometheus/procfs/internal/fs" -> "os"; +"github.com/prometheus/procfs/internal/fs" -> "path/filepath"; +"github.com/prometheus/procfs/internal/util" [label="github.com/prometheus/procfs/internal/util" color="paleturquoise" URL="https://godoc.org/github.com/prometheus/procfs/internal/util" target="_blank"]; +"github.com/prometheus/procfs/internal/util" -> "bytes"; +"github.com/prometheus/procfs/internal/util" -> "io/ioutil"; +"github.com/prometheus/procfs/internal/util" -> "os"; +"github.com/prometheus/procfs/internal/util" -> "strconv"; +"github.com/prometheus/procfs/internal/util" -> "strings"; +"github.com/prometheus/procfs/internal/util" -> "syscall"; +"hash" [label="hash" color="palegreen" URL="https://godoc.org/hash" target="_blank"]; +"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"]; +"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"]; +"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"]; +"math" [label="math" color="palegreen" URL="https://godoc.org/math" target="_blank"]; +"math/bits" [label="math/bits" color="palegreen" URL="https://godoc.org/math/bits" target="_blank"]; +"mime" [label="mime" color="palegreen" URL="https://godoc.org/mime" target="_blank"]; +"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"]; +"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"]; +"net/http/httptrace" [label="net/http/httptrace" color="palegreen" URL="https://godoc.org/net/http/httptrace" target="_blank"]; +"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"]; +"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"]; +"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"]; +"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"]; +"reflect" [label="reflect" color="palegreen" URL="https://godoc.org/reflect" target="_blank"]; +"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"]; +"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"]; +"runtime/debug" [label="runtime/debug" color="palegreen" URL="https://godoc.org/runtime/debug" target="_blank"]; +"sort" [label="sort" color="palegreen" URL="https://godoc.org/sort" target="_blank"]; +"strconv" [label="strconv" color="palegreen" URL="https://godoc.org/strconv" target="_blank"]; +"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"]; +"sync" [label="sync" color="palegreen" URL="https://godoc.org/sync" target="_blank"]; +"sync/atomic" [label="sync/atomic" color="palegreen" URL="https://godoc.org/sync/atomic" target="_blank"]; +"syscall" [label="syscall" color="palegreen" URL="https://godoc.org/syscall" target="_blank"]; +"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"]; +"unicode" [label="unicode" color="palegreen" URL="https://godoc.org/unicode" target="_blank"]; +"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"]; +"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"]; +} diff --git a/images/dot/ggcr.dot b/images/dot/ggcr.dot new file mode 100644 index 0000000..459ba6d --- /dev/null +++ b/images/dot/ggcr.dot @@ -0,0 +1,130 @@ +digraph godep { +nodesep=0.4 +ranksep=0.8 +node [shape="box",style="rounded,filled"] +edge [arrowsize="0.5"] +"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"]; +"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"]; +"context" [label="context" color="palegreen" URL="https://godoc.org/context" target="_blank"]; +"encoding/base64" [label="encoding/base64" color="palegreen" URL="https://godoc.org/encoding/base64" target="_blank"]; +"encoding/json" [label="encoding/json" color="palegreen" URL="https://godoc.org/encoding/json" target="_blank"]; +"errors" [label="errors" color="palegreen" URL="https://godoc.org/errors" target="_blank"]; +"fmt" [label="fmt" color="palegreen" URL="https://godoc.org/fmt" target="_blank"]; +"github.com/docker/cli/cli/config" [label="github.com/docker/cli/cli/config" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config" target="_blank"]; +"github.com/docker/cli/cli/config" -> "fmt"; +"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/configfile"; +"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/credentials"; +"github.com/docker/cli/cli/config" -> "github.com/docker/cli/cli/config/types"; +"github.com/docker/cli/cli/config" -> "github.com/docker/docker/pkg/homedir"; +"github.com/docker/cli/cli/config" -> "github.com/pkg/errors"; +"github.com/docker/cli/cli/config" -> "io"; +"github.com/docker/cli/cli/config" -> "os"; +"github.com/docker/cli/cli/config" -> "path/filepath"; +"github.com/docker/cli/cli/config" -> "strings"; +"github.com/docker/cli/cli/config/configfile" [label="github.com/docker/cli/cli/config/configfile" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/configfile" target="_blank"]; +"github.com/docker/cli/cli/config/configfile" -> "encoding/base64"; +"github.com/docker/cli/cli/config/configfile" -> "encoding/json"; +"github.com/docker/cli/cli/config/configfile" -> "fmt"; +"github.com/docker/cli/cli/config/configfile" -> "github.com/docker/cli/cli/config/credentials"; +"github.com/docker/cli/cli/config/configfile" -> "github.com/docker/cli/cli/config/types"; +"github.com/docker/cli/cli/config/configfile" -> "github.com/pkg/errors"; +"github.com/docker/cli/cli/config/configfile" -> "io"; +"github.com/docker/cli/cli/config/configfile" -> "io/ioutil"; +"github.com/docker/cli/cli/config/configfile" -> "os"; +"github.com/docker/cli/cli/config/configfile" -> "path/filepath"; +"github.com/docker/cli/cli/config/configfile" -> "strings"; +"github.com/docker/cli/cli/config/credentials" [label="github.com/docker/cli/cli/config/credentials" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/credentials" target="_blank"]; +"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/cli/cli/config/types"; +"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/docker-credential-helpers/client"; +"github.com/docker/cli/cli/config/credentials" -> "github.com/docker/docker-credential-helpers/credentials"; +"github.com/docker/cli/cli/config/credentials" -> "os/exec"; +"github.com/docker/cli/cli/config/credentials" -> "strings"; +"github.com/docker/cli/cli/config/types" [label="github.com/docker/cli/cli/config/types" color="paleturquoise" URL="https://godoc.org/github.com/docker/cli/cli/config/types" target="_blank"]; +"github.com/docker/docker-credential-helpers/client" [label="github.com/docker/docker-credential-helpers/client" color="palegoldenrod" URL="https://godoc.org/github.com/docker/docker-credential-helpers/client" target="_blank"]; +"github.com/docker/docker-credential-helpers/client" -> "bytes"; +"github.com/docker/docker-credential-helpers/client" -> "encoding/json"; +"github.com/docker/docker-credential-helpers/client" -> "fmt"; +"github.com/docker/docker-credential-helpers/client" -> "github.com/docker/docker-credential-helpers/credentials"; +"github.com/docker/docker-credential-helpers/client" -> "io"; +"github.com/docker/docker-credential-helpers/client" -> "os"; +"github.com/docker/docker-credential-helpers/client" -> "os/exec"; +"github.com/docker/docker-credential-helpers/client" -> "strings"; +"github.com/docker/docker-credential-helpers/credentials" [label="github.com/docker/docker-credential-helpers/credentials" color="palegoldenrod" URL="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" target="_blank"]; +"github.com/docker/docker-credential-helpers/credentials" -> "bufio"; +"github.com/docker/docker-credential-helpers/credentials" -> "bytes"; +"github.com/docker/docker-credential-helpers/credentials" -> "encoding/json"; +"github.com/docker/docker-credential-helpers/credentials" -> "fmt"; +"github.com/docker/docker-credential-helpers/credentials" -> "io"; +"github.com/docker/docker-credential-helpers/credentials" -> "os"; +"github.com/docker/docker-credential-helpers/credentials" -> "strings"; +"github.com/docker/docker/pkg/homedir" [label="github.com/docker/docker/pkg/homedir" color="paleturquoise" URL="https://godoc.org/github.com/docker/docker/pkg/homedir" target="_blank"]; +"github.com/docker/docker/pkg/homedir" -> "errors"; +"github.com/docker/docker/pkg/homedir" -> "os"; +"github.com/docker/docker/pkg/homedir" -> "os/user"; +"github.com/docker/docker/pkg/homedir" -> "path/filepath"; +"github.com/docker/docker/pkg/homedir" -> "strings"; +"github.com/google/go-containerregistry/pkg/authn" [label="github.com/google/go-containerregistry/pkg/authn" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/authn" target="_blank"]; +"github.com/google/go-containerregistry/pkg/authn" -> "encoding/json"; +"github.com/google/go-containerregistry/pkg/authn" -> "github.com/docker/cli/cli/config"; +"github.com/google/go-containerregistry/pkg/authn" -> "github.com/docker/cli/cli/config/types"; +"github.com/google/go-containerregistry/pkg/authn" -> "github.com/google/go-containerregistry/pkg/logs"; +"github.com/google/go-containerregistry/pkg/authn" -> "github.com/google/go-containerregistry/pkg/name"; +"github.com/google/go-containerregistry/pkg/authn" -> "os"; +"github.com/google/go-containerregistry/pkg/internal/retry" [label="github.com/google/go-containerregistry/pkg/internal/retry" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry" target="_blank"]; +"github.com/google/go-containerregistry/pkg/internal/retry" -> "context"; +"github.com/google/go-containerregistry/pkg/internal/retry" -> "fmt"; +"github.com/google/go-containerregistry/pkg/internal/retry" -> "github.com/google/go-containerregistry/pkg/internal/retry/wait"; +"github.com/google/go-containerregistry/pkg/internal/retry/wait" [label="github.com/google/go-containerregistry/pkg/internal/retry/wait" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry/wait" target="_blank"]; +"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "errors"; +"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "math/rand"; +"github.com/google/go-containerregistry/pkg/internal/retry/wait" -> "time"; +"github.com/google/go-containerregistry/pkg/logs" [label="github.com/google/go-containerregistry/pkg/logs" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/logs" target="_blank"]; +"github.com/google/go-containerregistry/pkg/logs" -> "io/ioutil"; +"github.com/google/go-containerregistry/pkg/logs" -> "log"; +"github.com/google/go-containerregistry/pkg/name" [label="github.com/google/go-containerregistry/pkg/name" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/name" target="_blank"]; +"github.com/google/go-containerregistry/pkg/name" -> "fmt"; +"github.com/google/go-containerregistry/pkg/name" -> "net"; +"github.com/google/go-containerregistry/pkg/name" -> "net/url"; +"github.com/google/go-containerregistry/pkg/name" -> "regexp"; +"github.com/google/go-containerregistry/pkg/name" -> "strings"; +"github.com/google/go-containerregistry/pkg/name" -> "unicode/utf8"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" [label="github.com/google/go-containerregistry/pkg/v1/remote/transport" color="paleturquoise" URL="https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport" target="_blank"]; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "encoding/base64"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "encoding/json"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "fmt"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/authn"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/internal/retry"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/logs"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "github.com/google/go-containerregistry/pkg/name"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "io/ioutil"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/http"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/http/httputil"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "net/url"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "strings"; +"github.com/google/go-containerregistry/pkg/v1/remote/transport" -> "time"; +"github.com/pkg/errors" [label="github.com/pkg/errors" color="palegoldenrod" URL="https://godoc.org/github.com/pkg/errors" target="_blank"]; +"github.com/pkg/errors" -> "fmt"; +"github.com/pkg/errors" -> "io"; +"github.com/pkg/errors" -> "path"; +"github.com/pkg/errors" -> "runtime"; +"github.com/pkg/errors" -> "strings"; +"io" [label="io" color="palegreen" URL="https://godoc.org/io" target="_blank"]; +"io/ioutil" [label="io/ioutil" color="palegreen" URL="https://godoc.org/io/ioutil" target="_blank"]; +"log" [label="log" color="palegreen" URL="https://godoc.org/log" target="_blank"]; +"math/rand" [label="math/rand" color="palegreen" URL="https://godoc.org/math/rand" target="_blank"]; +"net" [label="net" color="palegreen" URL="https://godoc.org/net" target="_blank"]; +"net/http" [label="net/http" color="palegreen" URL="https://godoc.org/net/http" target="_blank"]; +"net/http/httputil" [label="net/http/httputil" color="palegreen" URL="https://godoc.org/net/http/httputil" target="_blank"]; +"net/url" [label="net/url" color="palegreen" URL="https://godoc.org/net/url" target="_blank"]; +"os" [label="os" color="palegreen" URL="https://godoc.org/os" target="_blank"]; +"os/exec" [label="os/exec" color="palegreen" URL="https://godoc.org/os/exec" target="_blank"]; +"os/user" [label="os/user" color="palegreen" URL="https://godoc.org/os/user" target="_blank"]; +"path" [label="path" color="palegreen" URL="https://godoc.org/path" target="_blank"]; +"path/filepath" [label="path/filepath" color="palegreen" URL="https://godoc.org/path/filepath" target="_blank"]; +"regexp" [label="regexp" color="palegreen" URL="https://godoc.org/regexp" target="_blank"]; +"runtime" [label="runtime" color="palegreen" URL="https://godoc.org/runtime" target="_blank"]; +"strings" [label="strings" color="palegreen" URL="https://godoc.org/strings" target="_blank"]; +"time" [label="time" color="palegreen" URL="https://godoc.org/time" target="_blank"]; +"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"]; +} diff --git a/images/dot/image-anatomy.dot b/images/dot/image-anatomy.dot new file mode 100644 index 0000000..179e311 --- /dev/null +++ b/images/dot/image-anatomy.dot @@ -0,0 +1,26 @@ +digraph { + compound=true; + rankdir="LR"; + + tag [label="", shape="circle", width=0.1, style="filled", color="black"]; + manifest [shape="note"]; + config [shape="note"]; + + tag -> manifest [label="digest", taillabel="tag", tailport=head, labeldistance=2.1, labelangle=108]; + manifest -> config [label="(image id)"]; + config -> l1 [label="diffid"]; + config -> l2 [label="diffid"]; + manifest -> l1 [lhead=cluster_layer1, label="layer digest"]; + manifest -> l2 [lhead=cluster_layer2, label="layer digest"]; + + subgraph cluster_layer1 { + label = "layer.tar.gz"; + margin = 20.0; + l1 [label="layer.tar", shape="folder"]; + } + subgraph cluster_layer2 { + label = "layer.tar.gz"; + margin = 20.0; + l2 [label="layer.tar", shape="folder"]; + } +} diff --git a/images/dot/index-anatomy-strange.dot b/images/dot/index-anatomy-strange.dot new file mode 100644 index 0000000..2bccba3 --- /dev/null +++ b/images/dot/index-anatomy-strange.dot @@ -0,0 +1,24 @@ +digraph { + ordering = out; + compound=true; + rankdir="LR"; + + tag [label="", shape="circle", width=0.1, style="filled", color="black"]; + tag2 [label="", shape="circle", width=0.1, style="filled", color="black"]; + tag3 [label="", shape="circle", width=0.1, style="filled", color="black"]; + index [shape="note"]; + index2 [label="index", shape="note"]; + image [shape="note"]; + image2 [label="image", shape="note"]; + image3 [label="image", shape="note"]; + xml; + + tag -> index [taillabel="r124356", tailport=head, labeldistance=2.1, labelangle=108]; + tag2 -> index2 [taillabel="stable-release", tailport=head, labeldistance=2.1, labelangle=108]; + tag3 -> image [taillabel="v1.0", tailport=head, labeldistance=2.1, labelangle=108]; + index -> image; + index -> xml; + index -> index2; + index2 -> image2; + index2 -> image3; +} diff --git a/images/dot/index-anatomy.dot b/images/dot/index-anatomy.dot new file mode 100644 index 0000000..9155af0 --- /dev/null +++ b/images/dot/index-anatomy.dot @@ -0,0 +1,18 @@ +digraph { + ordering = out; + compound=true; + rankdir="LR"; + + tag [label="", shape="circle", width=0.1, style="filled", color="black"]; + tag2 [label="", shape="circle", width=0.1, style="filled", color="black"]; + tag3 [label="", shape="circle", width=0.1, style="filled", color="black"]; + index [shape="note"]; + image [shape="note"]; + image2 [label="image", shape="note"]; + + tag -> index [taillabel="latest", tailport=head, labeldistance=2.1, labelangle=108]; + tag2 -> image [taillabel="amd64", tailport=head, labeldistance=2.1, labelangle=108]; + tag3 -> image2 [taillabel="ppc64le", tailport=head, labeldistance=2.1, labelangle=252]; + index -> image; + index -> image2; +} diff --git a/images/dot/mutate.dot b/images/dot/mutate.dot new file mode 100644 index 0000000..228f8b6 --- /dev/null +++ b/images/dot/mutate.dot @@ -0,0 +1,59 @@ +digraph { + input [label="v1.Image", shape=box]; + output [label="v1.Image", shape=box]; + + ordering = "out"; + + subgraph cluster_source { + label = "Sources"; + "remotesource" [label="remote"]; + "tarballsource" [label="tarball"]; + "randomsource" [label="random"]; + "layoutsource" [label="layout"]; + "daemonsource" [label="daemon"]; + } + + subgraph cluster_mutate { + label = "mutate"; + "mutateconfig" [label="Config"]; + "mutatetime" [label="Time"]; + "mutatemediatype" [label="MediaType"]; + "mutateappend" [label="Append"]; + "mutaterebase" [label="Rebase"]; + } + + subgraph cluster_sinks { + label = "Sinks"; + labelloc = "b"; + + "remotesink" [label="remote"]; + "tarballsink" [label="tarball"]; + "legacy/tarballsink" [label="legacy/tarball"]; + "layoutsink" [label="layout"]; + "daemonsink" [label="daemon"]; + } + + "randomsource" -> input; + "layoutsource" -> input; + "daemonsource" -> input; + "tarballsource" -> input; + "remotesource" -> input; + + input -> "mutateconfig"; + input -> "mutatetime"; + input -> "mutatemediatype"; + input -> "mutateappend"; + input -> "mutaterebase"; + + "mutateconfig" -> output; + "mutatetime" -> output; + "mutatemediatype" -> output; + "mutateappend" -> output; + "mutaterebase" -> output; + + output -> "legacy/tarballsink"; + output -> "layoutsink"; + output -> "daemonsink"; + output -> "tarballsink"; + output -> "remotesink"; +} diff --git a/images/dot/remote.dot b/images/dot/remote.dot new file mode 100644 index 0000000..9b5e08c --- /dev/null +++ b/images/dot/remote.dot @@ -0,0 +1,66 @@ +digraph { + compound=true; + rankdir="LR"; + ordering = in; + + subgraph cluster_registry { + label = "registry"; + + subgraph cluster_tags { + label = "/v2/.../tags/list"; + + tag [label="tag", shape="rect"]; + tag2 [label="tag", shape="rect"]; + } + + subgraph cluster_manifests { + label = "/v2/.../manifests/<ref>"; + + subgraph cluster_manifest { + label = "manifest"; + + mconfig [label="config", shape="rect"]; + layers [label="layers", shape="rect"]; + } + + subgraph cluster_manifest2 { + label = "manifest"; + + mconfig2 [label="config", shape="rect"]; + layers2 [label="layers", shape="rect"]; + } + + subgraph cluster_index { + label = "index"; + + imanifest [label="manifests", shape="rect"]; + } + + imanifest -> mconfig [lhead=cluster_manifest]; + imanifest -> mconfig2 [lhead=cluster_manifest2]; + } + + subgraph cluster_blobs { + label = "/v2/.../blobs/<sha256>"; + + bconfig [label="config", shape="hexagon"]; + bconfig2 [label="config", shape="hexagon"]; + + l1 [label="layer", shape="folder"]; + l2 [label="layer", shape="folder"]; + l3 [label="layer", shape="folder"]; + } + + layers -> l1; + layers -> l2; + + layers2 -> l2; + layers2 -> l3; + + mconfig -> bconfig; + mconfig2 -> bconfig2; + + tag -> mconfig [style="dashed", lhead=cluster_manifest]; + tag2 -> imanifest [style="dashed", lhead=cluster_index]; + } +} diff --git a/images/dot/stream.dot b/images/dot/stream.dot new file mode 100644 index 0000000..0987be7 --- /dev/null +++ b/images/dot/stream.dot @@ -0,0 +1,47 @@ +digraph G { + ordering=out; + + fs [label="input", shape="folder"]; + pr [label="io.PipeReader"]; + compressed [label="Compressed()", shape="rect"]; + rc2 [label="io.ReadCloser"]; + output [label="output", shape="cylinder"]; + + subgraph cluster_goroutine { + label = "goroutine"; + + rc [label="io.ReadCloser"]; + copy [label="io.Copy"]; + pw [label="io.PipeWriter"]; + mw [label="io.MultiWriter"]; + h1 [label="sha256.New"]; + gzip [label="gzip.Writer"]; + mw2 [label="io.MultiWriter"]; + h2 [label="sha256.New"]; + count [label="countWriter"]; + + size [label="Size()", shape="rect"]; + diffid [label="DiffID()", shape="rect"]; + digest [label="Digest()", shape="rect"]; + + + rc -> copy [style="bold"]; + copy -> mw [style="bold"]; + mw -> h1; + h1 -> diffid [style="dashed"]; + mw -> gzip [style="bold"]; + gzip -> mw2 [style="bold"]; + mw2 -> h2; + h2 -> digest [style="dashed"]; + mw2 -> count; + count -> size [style="dotted"]; + mw2 -> pw [style="bold"]; + }; + + fs -> rc [style="bold"]; + + pw -> pr [style="bold"]; + pr -> compressed [style="bold"]; + compressed -> rc2 [style="bold"]; + rc2 -> output [style="bold"]; +} diff --git a/images/dot/tarball.dot b/images/dot/tarball.dot new file mode 100644 index 0000000..595283f --- /dev/null +++ b/images/dot/tarball.dot @@ -0,0 +1,43 @@ +digraph { + compound=true; + rankdir="LR"; + ordering = out; + + subgraph cluster_tarball { + label = "image.tar"; + + subgraph cluster_manifest { + label = "manifest.json"; + + mconfig [label="Config", shape="rect"]; + layers [label="Layers", shape="rect"]; + sources [label="LayerSources", shape="rect"]; + tags [label="RepoTags", shape="rect"]; + } + + config [shape="note"]; + + mconfig -> config [label="image id"]; + + layers -> l1 [lhead=cluster_layer1, label="layer digest"]; + layers -> l2 [lhead=cluster_layer2, label="layer digest"]; + + config -> l1 [label="diffid"]; + config -> l2 [label="diffid"]; + + sources -> l1 [label="diffid"]; + sources -> l2 [label="diffid"]; + + subgraph cluster_layer1 { + label = "layer.tar.gz"; + margin = 20.0; + l1 [label="layer.tar", shape="folder"]; + } + + subgraph cluster_layer2 { + label = "layer.tar.gz"; + margin = 20.0; + l2 [label="layer.tar", shape="folder"]; + } + } +} diff --git a/images/dot/upload.dot b/images/dot/upload.dot new file mode 100644 index 0000000..2cb3e26 --- /dev/null +++ b/images/dot/upload.dot @@ -0,0 +1,67 @@ +digraph G { + ordering=out; + + fs [label="filesystem\nchangeset", shape=folder, href="https://github.com/opencontainers/image-spec/blob/master/layer.md"]; + configuration [label="image\nconfig", shape=hexagon, href="https://github.com/opencontainers/image-spec/blob/master/config.md#properties"]; + + tar [shape=rect]; + gzip [shape=rect]; + tee [shape=rect]; + tee2 [label=tee, shape=rect]; + tee3 [label=tee, shape=rect]; + sha256sum [shape=rect]; + sha256sum2 [label=sha256sum, shape=rect]; + sha256sum3 [label=sha256sum, shape=rect]; + curl [shape=rect]; + curl2 [label=curl, shape=rect]; + curl3 [label=curl, shape=rect]; + wc [label="wc -c", shape=rect]; + wc2 [label="wc -c", shape=rect]; + + config [label="config file", shape=note, href="https://github.com/opencontainers/image-spec/blob/master/config.md"]; + layer [shape=note, href="https://github.com/opencontainers/image-spec/blob/master/layer.md"]; + manifest [shape=note, href="https://github.com/opencontainers/image-spec/blob/master/manifest.md"]; + + registry [shape=cylinder, href="https://github.com/opencontainers/distribution-spec/blob/master/spec.md"]; + + config_size [label="config size"]; + layer_size [label="layer size"]; + config_digest [label="config digest\n(image id)", href="https://github.com/opencontainers/image-spec/blob/master/config.md#imageid"]; + layer_digest [label="layer digest"]; + + diffid [href="https://github.com/opencontainers/image-spec/blob/master/config.md#layer-diffid"]; + + configuration -> config; + fs -> tar; + + tar -> tee; + tee -> sha256sum; + sha256sum -> diffid [style=dashed]; + tee -> gzip; + gzip -> layer; + layer -> tee2; + tee2 -> sha256sum2; + sha256sum2 -> layer_digest [style=dashed]; + tee2 -> wc; + wc -> layer_size [style=dotted]; + layer_size -> manifest [style=dotted]; + tee2 -> curl; + + curl -> registry; + + diffid -> config [style=dashed]; + config -> tee3; + tee3 -> curl2; + curl2 -> registry; + + tee3 -> wc2; + tee3 -> sha256sum3; + wc2 -> config_size [style=dotted]; + sha256sum3 -> config_digest [style=dashed]; + + config_digest -> manifest [style=dashed]; + config_size -> manifest [style=dotted]; + layer_digest -> manifest [style=dashed]; + manifest -> curl3; + curl3 -> registry; +} diff --git a/images/gcrane.png b/images/gcrane.png Binary files differnew file mode 100644 index 0000000..461fbfe --- /dev/null +++ b/images/gcrane.png diff --git a/images/ggcr.dot.svg b/images/ggcr.dot.svg new file mode 100644 index 0000000..3dbeccd --- /dev/null +++ b/images/ggcr.dot.svg @@ -0,0 +1,874 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: godep Pages: 1 --> +<svg width="3212pt" height="702pt" + viewBox="0.00 0.00 3211.50 702.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 698)"> +<title>godep</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-698 3207.5,-698 3207.5,4 -4,4"/> +<!-- bufio --> +<g id="node1" class="node"> +<title>bufio</title> +<g id="a_node1"><a xlink:href="https://godoc.org/bufio" xlink:title="bufio" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M828,-36C828,-36 798,-36 798,-36 792,-36 786,-30 786,-24 786,-24 786,-12 786,-12 786,-6 792,0 798,0 798,0 828,0 828,0 834,0 840,-6 840,-12 840,-12 840,-24 840,-24 840,-30 834,-36 828,-36"/> +<text text-anchor="middle" x="813" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bufio</text> +</a> +</g> +</g> +<!-- bytes --> +<g id="node2" class="node"> +<title>bytes</title> +<g id="a_node2"><a xlink:href="https://godoc.org/bytes" xlink:title="bytes" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M683,-36C683,-36 653,-36 653,-36 647,-36 641,-30 641,-24 641,-24 641,-12 641,-12 641,-6 647,0 653,0 653,0 683,0 683,0 689,0 695,-6 695,-12 695,-12 695,-24 695,-24 695,-30 689,-36 683,-36"/> +<text text-anchor="middle" x="668" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">bytes</text> +</a> +</g> +</g> +<!-- context --> +<g id="node3" class="node"> +<title>context</title> +<g id="a_node3"><a xlink:href="https://godoc.org/context" xlink:title="context" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2528,-506C2528,-506 2496,-506 2496,-506 2490,-506 2484,-500 2484,-494 2484,-494 2484,-482 2484,-482 2484,-476 2490,-470 2496,-470 2496,-470 2528,-470 2528,-470 2534,-470 2540,-476 2540,-482 2540,-482 2540,-494 2540,-494 2540,-500 2534,-506 2528,-506"/> +<text text-anchor="middle" x="2512" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">context</text> +</a> +</g> +</g> +<!-- encoding/base64 --> +<g id="node4" class="node"> +<title>encoding/base64</title> +<g id="a_node4"><a xlink:href="https://godoc.org/encoding/base64" xlink:title="encoding/base64" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M257.5,-318C257.5,-318 174.5,-318 174.5,-318 168.5,-318 162.5,-312 162.5,-306 162.5,-306 162.5,-294 162.5,-294 162.5,-288 168.5,-282 174.5,-282 174.5,-282 257.5,-282 257.5,-282 263.5,-282 269.5,-288 269.5,-294 269.5,-294 269.5,-306 269.5,-306 269.5,-312 263.5,-318 257.5,-318"/> +<text text-anchor="middle" x="216" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/base64</text> +</a> +</g> +</g> +<!-- encoding/json --> +<g id="node5" class="node"> +<title>encoding/json</title> +<g id="a_node5"><a xlink:href="https://godoc.org/encoding/json" xlink:title="encoding/json" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M228,-36C228,-36 160,-36 160,-36 154,-36 148,-30 148,-24 148,-24 148,-12 148,-12 148,-6 154,0 160,0 160,0 228,0 228,0 234,0 240,-6 240,-12 240,-12 240,-24 240,-24 240,-30 234,-36 228,-36"/> +<text text-anchor="middle" x="194" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">encoding/json</text> +</a> +</g> +</g> +<!-- errors --> +<g id="node6" class="node"> +<title>errors</title> +<g id="a_node6"><a xlink:href="https://godoc.org/errors" xlink:title="errors" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2236,-318C2236,-318 2206,-318 2206,-318 2200,-318 2194,-312 2194,-306 2194,-306 2194,-294 2194,-294 2194,-288 2200,-282 2206,-282 2206,-282 2236,-282 2236,-282 2242,-282 2248,-288 2248,-294 2248,-294 2248,-306 2248,-306 2248,-312 2242,-318 2236,-318"/> +<text text-anchor="middle" x="2221" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">errors</text> +</a> +</g> +</g> +<!-- fmt --> +<g id="node7" class="node"> +<title>fmt</title> +<g id="a_node7"><a xlink:href="https://godoc.org/fmt" xlink:title="fmt" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1986,-36C1986,-36 1956,-36 1956,-36 1950,-36 1944,-30 1944,-24 1944,-24 1944,-12 1944,-12 1944,-6 1950,0 1956,0 1956,0 1986,0 1986,0 1992,0 1998,-6 1998,-12 1998,-12 1998,-24 1998,-24 1998,-30 1992,-36 1986,-36"/> +<text text-anchor="middle" x="1971" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">fmt</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config --> +<g id="node8" class="node"> +<title>github.com/docker/cli/cli/config</title> +<g id="a_node8"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config" xlink:title="github.com/docker/cli/cli/config" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1254,-506C1254,-506 1086,-506 1086,-506 1080,-506 1074,-500 1074,-494 1074,-494 1074,-482 1074,-482 1074,-476 1080,-470 1086,-470 1086,-470 1254,-470 1254,-470 1260,-470 1266,-476 1266,-482 1266,-482 1266,-494 1266,-494 1266,-500 1260,-506 1254,-506"/> +<text text-anchor="middle" x="1170" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->fmt --> +<g id="edge1" class="edge"> +<title>github.com/docker/cli/cli/config->fmt</title> +<path fill="none" stroke="#000000" d="M1266.2661,-477.1263C1373.9923,-463.8751 1540.9149,-439.7838 1598,-412 1777.8388,-324.4709 1915.961,-111.0793 1957.8445,-40.8353"/> +<polygon fill="#000000" stroke="#000000" points="1959.4186,-41.6117 1960.4635,-36.4184 1956.4081,-39.8266 1959.4186,-41.6117"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile --> +<g id="node9" class="node"> +<title>github.com/docker/cli/cli/config/configfile</title> +<g id="a_node9"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/configfile" xlink:title="github.com/docker/cli/cli/config/configfile" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1084,-412C1084,-412 860,-412 860,-412 854,-412 848,-406 848,-400 848,-400 848,-388 848,-388 848,-382 854,-376 860,-376 860,-376 1084,-376 1084,-376 1090,-376 1096,-382 1096,-388 1096,-388 1096,-400 1096,-400 1096,-406 1090,-412 1084,-412"/> +<text text-anchor="middle" x="972" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/configfile</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/configfile --> +<g id="edge2" class="edge"> +<title>github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/configfile</title> +<path fill="none" stroke="#000000" d="M1131.8236,-469.8759C1098.2688,-453.9458 1049.4939,-430.79 1014.6636,-414.2545"/> +<polygon fill="#000000" stroke="#000000" points="1015.3266,-412.632 1010.0592,-412.0685 1013.8255,-415.7938 1015.3266,-412.632"/> +</g> +<!-- github.com/docker/cli/cli/config/credentials --> +<g id="node10" class="node"> +<title>github.com/docker/cli/cli/config/credentials</title> +<g id="a_node10"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/credentials" xlink:title="github.com/docker/cli/cli/config/credentials" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M925,-318C925,-318 695,-318 695,-318 689,-318 683,-312 683,-306 683,-306 683,-294 683,-294 683,-288 689,-282 695,-282 695,-282 925,-282 925,-282 931,-282 937,-288 937,-294 937,-294 937,-306 937,-306 937,-312 931,-318 925,-318"/> +<text text-anchor="middle" x="810" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/credentials</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/credentials --> +<g id="edge3" class="edge"> +<title>github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/credentials</title> +<path fill="none" stroke="#000000" d="M1073.825,-477.9324C986.3917,-466.7805 866.1268,-445.7713 833,-412 810.0302,-388.5833 807.1309,-348.7312 807.962,-323.4184"/> +<polygon fill="#000000" stroke="#000000" points="809.7179,-323.3211 808.19,-318.2488 806.2213,-323.1667 809.7179,-323.3211"/> +</g> +<!-- github.com/docker/cli/cli/config/types --> +<g id="node11" class="node"> +<title>github.com/docker/cli/cli/config/types</title> +<g id="a_node11"><a xlink:href="https://godoc.org/github.com/docker/cli/cli/config/types" xlink:title="github.com/docker/cli/cli/config/types" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1247,-224C1247,-224 1047,-224 1047,-224 1041,-224 1035,-218 1035,-212 1035,-212 1035,-200 1035,-200 1035,-194 1041,-188 1047,-188 1047,-188 1247,-188 1247,-188 1253,-188 1259,-194 1259,-200 1259,-200 1259,-212 1259,-212 1259,-218 1253,-224 1247,-224"/> +<text text-anchor="middle" x="1147" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/cli/cli/config/types</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/types --> +<g id="edge4" class="edge"> +<title>github.com/docker/cli/cli/config->github.com/docker/cli/cli/config/types</title> +<path fill="none" stroke="#000000" d="M1168.5263,-469.9306C1164.5114,-420.7051 1153.3823,-284.252 1148.9034,-229.3379"/> +<polygon fill="#000000" stroke="#000000" points="1150.642,-229.125 1148.4912,-224.2838 1147.1536,-229.4096 1150.642,-229.125"/> +</g> +<!-- github.com/docker/docker/pkg/homedir --> +<g id="node12" class="node"> +<title>github.com/docker/docker/pkg/homedir</title> +<g id="a_node12"><a xlink:href="https://godoc.org/github.com/docker/docker/pkg/homedir" xlink:title="github.com/docker/docker/pkg/homedir" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1464.5,-412C1464.5,-412 1255.5,-412 1255.5,-412 1249.5,-412 1243.5,-406 1243.5,-400 1243.5,-400 1243.5,-388 1243.5,-388 1243.5,-382 1249.5,-376 1255.5,-376 1255.5,-376 1464.5,-376 1464.5,-376 1470.5,-376 1476.5,-382 1476.5,-388 1476.5,-388 1476.5,-400 1476.5,-400 1476.5,-406 1470.5,-412 1464.5,-412"/> +<text text-anchor="middle" x="1360" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker/pkg/homedir</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->github.com/docker/docker/pkg/homedir --> +<g id="edge5" class="edge"> +<title>github.com/docker/cli/cli/config->github.com/docker/docker/pkg/homedir</title> +<path fill="none" stroke="#000000" d="M1206.6339,-469.8759C1238.7015,-454.0108 1285.2554,-430.9789 1318.6502,-414.4573"/> +<polygon fill="#000000" stroke="#000000" points="1319.773,-415.8543 1323.4786,-412.0685 1318.221,-412.7172 1319.773,-415.8543"/> +</g> +<!-- github.com/pkg/errors --> +<g id="node13" class="node"> +<title>github.com/pkg/errors</title> +<g id="a_node13"><a xlink:href="https://godoc.org/github.com/pkg/errors" xlink:title="github.com/pkg/errors" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M1709,-130C1709,-130 1595,-130 1595,-130 1589,-130 1583,-124 1583,-118 1583,-118 1583,-106 1583,-106 1583,-100 1589,-94 1595,-94 1595,-94 1709,-94 1709,-94 1715,-94 1721,-100 1721,-106 1721,-106 1721,-118 1721,-118 1721,-124 1715,-130 1709,-130"/> +<text text-anchor="middle" x="1652" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/pkg/errors</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->github.com/pkg/errors --> +<g id="edge6" class="edge"> +<title>github.com/docker/cli/cli/config->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M1266.0328,-479.5512C1343.3902,-470.1597 1453.5964,-450.8479 1543,-412 1576.0277,-397.6487 1658.4991,-350.008 1675,-318 1706.1362,-257.6029 1678.8276,-174.2295 1662.3518,-134.633"/> +<polygon fill="#000000" stroke="#000000" points="1663.962,-133.9476 1660.4009,-130.0257 1660.739,-135.3123 1663.962,-133.9476"/> +</g> +<!-- io --> +<g id="node14" class="node"> +<title>io</title> +<g id="a_node14"><a xlink:href="https://godoc.org/io" xlink:title="io" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1034,-36C1034,-36 1004,-36 1004,-36 998,-36 992,-30 992,-24 992,-24 992,-12 992,-12 992,-6 998,0 1004,0 1004,0 1034,0 1034,0 1040,0 1046,-6 1046,-12 1046,-12 1046,-24 1046,-24 1046,-30 1040,-36 1034,-36"/> +<text text-anchor="middle" x="1019" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">io</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->io --> +<g id="edge7" class="edge"> +<title>github.com/docker/cli/cli/config->io</title> +<path fill="none" stroke="#000000" d="M1073.6611,-480.3086C896.553,-463.8416 530.9911,-418.2188 462,-318 390.1382,-213.6112 368.5322,-243.7443 637,-94 697.5302,-60.2378 906.0734,-31.813 986.764,-21.8273"/> +<polygon fill="#000000" stroke="#000000" points="987.1423,-23.5441 991.8911,-21.1964 986.7147,-20.0703 987.1423,-23.5441"/> +</g> +<!-- os --> +<g id="node15" class="node"> +<title>os</title> +<g id="a_node15"><a xlink:href="https://godoc.org/os" xlink:title="os" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M533,-36C533,-36 503,-36 503,-36 497,-36 491,-30 491,-24 491,-24 491,-12 491,-12 491,-6 497,0 503,0 503,0 533,0 533,0 539,0 545,-6 545,-12 545,-12 545,-24 545,-24 545,-30 539,-36 533,-36"/> +<text text-anchor="middle" x="518" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">os</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->os --> +<g id="edge8" class="edge"> +<title>github.com/docker/cli/cli/config->os</title> +<path fill="none" stroke="#000000" d="M1073.7305,-482.9164C922.5411,-473.8315 637.0683,-451.9178 544,-412 438.4175,-366.7147 384.0948,-298.6494 415,-188 431.3471,-129.4728 473.9404,-71.0262 498.9374,-40.242"/> +<polygon fill="#000000" stroke="#000000" points="500.38,-41.2425 502.194,-36.2654 497.6721,-39.025 500.38,-41.2425"/> +</g> +<!-- path/filepath --> +<g id="node16" class="node"> +<title>path/filepath</title> +<g id="a_node16"><a xlink:href="https://godoc.org/path/filepath" xlink:title="path/filepath" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1393.5,-318C1393.5,-318 1332.5,-318 1332.5,-318 1326.5,-318 1320.5,-312 1320.5,-306 1320.5,-306 1320.5,-294 1320.5,-294 1320.5,-288 1326.5,-282 1332.5,-282 1332.5,-282 1393.5,-282 1393.5,-282 1399.5,-282 1405.5,-288 1405.5,-294 1405.5,-294 1405.5,-306 1405.5,-306 1405.5,-312 1399.5,-318 1393.5,-318"/> +<text text-anchor="middle" x="1363" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">path/filepath</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->path/filepath --> +<g id="edge9" class="edge"> +<title>github.com/docker/cli/cli/config->path/filepath</title> +<path fill="none" stroke="#000000" d="M1175.9422,-469.7514C1184.489,-445.8127 1202.2302,-403.615 1229,-376 1253.2845,-350.9488 1287.6155,-331.6936 1315.4478,-318.8326"/> +<polygon fill="#000000" stroke="#000000" points="1316.3842,-320.3292 1320.2098,-316.6648 1314.934,-317.1437 1316.3842,-320.3292"/> +</g> +<!-- strings --> +<g id="node17" class="node"> +<title>strings</title> +<g id="a_node17"><a xlink:href="https://godoc.org/strings" xlink:title="strings" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1495,-36C1495,-36 1465,-36 1465,-36 1459,-36 1453,-30 1453,-24 1453,-24 1453,-12 1453,-12 1453,-6 1459,0 1465,0 1465,0 1495,0 1495,0 1501,0 1507,-6 1507,-12 1507,-12 1507,-24 1507,-24 1507,-30 1501,-36 1495,-36"/> +<text text-anchor="middle" x="1480" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">strings</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config->strings --> +<g id="edge10" class="edge"> +<title>github.com/docker/cli/cli/config->strings</title> +<path fill="none" stroke="#000000" d="M1266.0054,-475.5835C1331.5889,-464.6593 1419.2633,-445.1657 1491,-412 1515.5289,-400.6597 1518.034,-391.7406 1540,-376 1576.5806,-349.7866 1602.2009,-357.9084 1623,-318 1650.6687,-264.9104 1539.5129,-100.7592 1496.4199,-40.4902"/> +<polygon fill="#000000" stroke="#000000" points="1497.7743,-39.3759 1493.4377,-36.3334 1494.9304,-41.4161 1497.7743,-39.3759"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->encoding/base64 --> +<g id="edge11" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->encoding/base64</title> +<path fill="none" stroke="#000000" d="M847.5855,-378.5305C682.4277,-357.995 397.3991,-322.5549 274.8279,-307.3146"/> +<polygon fill="#000000" stroke="#000000" points="275.0031,-305.573 269.8253,-306.6926 274.5711,-309.0462 275.0031,-305.573"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->encoding/json --> +<g id="edge12" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->encoding/json</title> +<path fill="none" stroke="#000000" d="M847.9491,-382.0576C774.096,-371.6871 679.9,-352.6298 602,-318 591.2618,-313.2264 313.7424,-107.0739 222.6591,-39.3267"/> +<polygon fill="#000000" stroke="#000000" points="223.5208,-37.7867 218.4645,-36.2066 221.4319,-40.595 223.5208,-37.7867"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->fmt --> +<g id="edge13" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->fmt</title> +<path fill="none" stroke="#000000" d="M1052.3679,-375.9376C1106.1723,-362.6836 1178.0334,-342.7178 1239,-318 1270.3273,-305.2989 1274.7303,-294.8422 1306,-282 1390.4286,-247.326 1415.6459,-250.4524 1503,-224 1637.5405,-183.2588 1678.2773,-190.8469 1805,-130 1857.9384,-104.5812 1913.2882,-63.9447 1944.8193,-39.2393"/> +<polygon fill="#000000" stroke="#000000" points="1946.074,-40.4788 1948.9199,-36.0108 1943.9088,-37.7288 1946.074,-40.4788"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->github.com/docker/cli/cli/config/credentials --> +<g id="edge14" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->github.com/docker/cli/cli/config/credentials</title> +<path fill="none" stroke="#000000" d="M940.7648,-375.8759C913.535,-360.0759 874.0548,-337.1676 845.6068,-320.6607"/> +<polygon fill="#000000" stroke="#000000" points="846.3423,-319.0643 841.1393,-318.0685 844.5857,-322.0916 846.3423,-319.0643"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->github.com/docker/cli/cli/config/types --> +<g id="edge15" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->github.com/docker/cli/cli/config/types</title> +<path fill="none" stroke="#000000" d="M975.6658,-375.8069C981.259,-351.9297 993.9088,-309.7997 1018,-282 1039.1613,-257.5811 1070.1883,-238.957 1096.5102,-226.2577"/> +<polygon fill="#000000" stroke="#000000" points="1097.2618,-227.8381 1101.0284,-224.1132 1095.761,-224.6762 1097.2618,-227.8381"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->github.com/pkg/errors --> +<g id="edge16" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->github.com/pkg/errors</title> +<path fill="none" stroke="#000000" d="M1026.0251,-375.9647C1068.3593,-361.4247 1128.5104,-339.8634 1180,-318 1213.6651,-303.7052 1219.9709,-295.4052 1254,-282 1335.7517,-249.7952 1361.8945,-257.7994 1443,-224 1507.3434,-197.1859 1578.0071,-156.8428 1618.4792,-132.5995"/> +<polygon fill="#000000" stroke="#000000" points="1619.3964,-134.0901 1622.7812,-130.015 1617.594,-131.0898 1619.3964,-134.0901"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->io --> +<g id="edge17" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->io</title> +<path fill="none" stroke="#000000" d="M995.2265,-375.9408C1026.1785,-352.399 1083.1956,-310.8353 1136,-282 1194.0319,-250.3101 1236.4794,-279.1199 1273,-224 1337.6707,-126.3937 1133.3242,-52.3646 1051.4109,-27.2658"/> +<polygon fill="#000000" stroke="#000000" points="1051.4894,-25.4609 1046.1967,-25.6836 1050.4731,-28.8101 1051.4894,-25.4609"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->os --> +<g id="edge19" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->os</title> +<path fill="none" stroke="#000000" d="M847.6594,-376.8602C790.5991,-365.5592 723.4612,-347.243 668,-318 612.3013,-288.6318 593.7166,-278.3958 562,-224 527.3587,-164.5882 519.8684,-81.2996 518.3325,-41.2744"/> +<polygon fill="#000000" stroke="#000000" points="520.0774,-41.0891 518.1597,-36.1509 516.5793,-41.2071 520.0774,-41.0891"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->path/filepath --> +<g id="edge20" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->path/filepath</title> +<path fill="none" stroke="#000000" d="M1053.6144,-375.9512C1119.6914,-361.1035 1215.0776,-339.1379 1298,-318 1303.6733,-316.5538 1309.61,-314.9859 1315.4908,-313.399"/> +<polygon fill="#000000" stroke="#000000" points="1316.0435,-315.0624 1320.4106,-312.0637 1315.1267,-311.6846 1316.0435,-315.0624"/> +</g> +<!-- github.com/docker/cli/cli/config/configfile->strings --> +<g id="edge21" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->strings</title> +<path fill="none" stroke="#000000" d="M1015.4995,-375.9692C1047.9599,-361.8316 1092.9141,-340.7881 1130,-318 1152.4184,-304.2246 1154.4863,-295.6191 1177,-282 1229.4026,-250.3002 1253.1077,-260.8843 1302,-224 1374.0841,-169.6198 1437.7085,-81.6836 1465.3568,-40.557"/> +<polygon fill="#000000" stroke="#000000" points="1466.9108,-41.3811 1468.2344,-36.2517 1464.001,-39.4361 1466.9108,-41.3811"/> +</g> +<!-- io/ioutil --> +<g id="node18" class="node"> +<title>io/ioutil</title> +<g id="a_node18"><a xlink:href="https://godoc.org/io/ioutil" xlink:title="io/ioutil" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M523.5,-318C523.5,-318 488.5,-318 488.5,-318 482.5,-318 476.5,-312 476.5,-306 476.5,-306 476.5,-294 476.5,-294 476.5,-288 482.5,-282 488.5,-282 488.5,-282 523.5,-282 523.5,-282 529.5,-282 535.5,-288 535.5,-294 535.5,-294 535.5,-306 535.5,-306 535.5,-312 529.5,-318 523.5,-318"/> +<text text-anchor="middle" x="506" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">io/ioutil</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config/configfile->io/ioutil --> +<g id="edge18" class="edge"> +<title>github.com/docker/cli/cli/config/configfile->io/ioutil</title> +<path fill="none" stroke="#000000" d="M847.7343,-376.2349C764.9442,-363.1676 654.1093,-343.2996 558,-318 552.298,-316.499 546.323,-314.6756 540.538,-312.7723"/> +<polygon fill="#000000" stroke="#000000" points="541.0304,-311.0917 535.7334,-311.1602 539.917,-314.4099 541.0304,-311.0917"/> +</g> +<!-- github.com/docker/cli/cli/config/credentials->github.com/docker/cli/cli/config/types --> +<g id="edge22" class="edge"> +<title>github.com/docker/cli/cli/config/credentials->github.com/docker/cli/cli/config/types</title> +<path fill="none" stroke="#000000" d="M874.5782,-281.9871C932.6702,-265.7834 1017.7991,-242.0382 1077.3897,-225.4165"/> +<polygon fill="#000000" stroke="#000000" points="1078.0077,-227.061 1082.3537,-224.0319 1077.0673,-223.6897 1078.0077,-227.061"/> +</g> +<!-- github.com/docker/cli/cli/config/credentials->strings --> +<g id="edge26" class="edge"> +<title>github.com/docker/cli/cli/config/credentials->strings</title> +<path fill="none" stroke="#000000" d="M849.5573,-281.9865C881.0458,-267.2893 925.9846,-245.5089 964,-224 989.752,-209.4297 993.3131,-200.7777 1020,-188 1093.5234,-152.797 1117.356,-157.7604 1194,-130 1287.0119,-96.3112 1395.41,-52.6325 1448.0494,-31.1342"/> +<polygon fill="#000000" stroke="#000000" points="1449.001,-32.6358 1452.9671,-29.124 1447.6767,-29.396 1449.001,-32.6358"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client --> +<g id="node19" class="node"> +<title>github.com/docker/docker-credential-helpers/client</title> +<g id="a_node19"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/client" xlink:title="github.com/docker/docker-credential-helpers/client" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M875.5,-224C875.5,-224 604.5,-224 604.5,-224 598.5,-224 592.5,-218 592.5,-212 592.5,-212 592.5,-200 592.5,-200 592.5,-194 598.5,-188 604.5,-188 604.5,-188 875.5,-188 875.5,-188 881.5,-188 887.5,-194 887.5,-200 887.5,-200 887.5,-212 887.5,-212 887.5,-218 881.5,-224 875.5,-224"/> +<text text-anchor="middle" x="740" y="-202.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker-credential-helpers/client</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config/credentials->github.com/docker/docker-credential-helpers/client --> +<g id="edge23" class="edge"> +<title>github.com/docker/cli/cli/config/credentials->github.com/docker/docker-credential-helpers/client</title> +<path fill="none" stroke="#000000" d="M796.5033,-281.8759C785.0763,-266.531 768.6564,-244.4815 756.4596,-228.1029"/> +<polygon fill="#000000" stroke="#000000" points="757.8452,-227.0335 753.4553,-224.0685 755.0381,-229.124 757.8452,-227.0335"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials --> +<g id="node20" class="node"> +<title>github.com/docker/docker-credential-helpers/credentials</title> +<g id="a_node20"><a xlink:href="https://godoc.org/github.com/docker/docker-credential-helpers/credentials" xlink:title="github.com/docker/docker-credential-helpers/credentials" target="_blank"> +<path fill="#eee8aa" stroke="#eee8aa" d="M962.5,-130C962.5,-130 663.5,-130 663.5,-130 657.5,-130 651.5,-124 651.5,-118 651.5,-118 651.5,-106 651.5,-106 651.5,-100 657.5,-94 663.5,-94 663.5,-94 962.5,-94 962.5,-94 968.5,-94 974.5,-100 974.5,-106 974.5,-106 974.5,-118 974.5,-118 974.5,-124 968.5,-130 962.5,-130"/> +<text text-anchor="middle" x="813" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/docker/docker-credential-helpers/credentials</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config/credentials->github.com/docker/docker-credential-helpers/credentials --> +<g id="edge24" class="edge"> +<title>github.com/docker/cli/cli/config/credentials->github.com/docker/docker-credential-helpers/credentials</title> +<path fill="none" stroke="#000000" d="M682.8452,-282.0149C610.9277,-266.1393 541.95,-237.5471 578,-188 595.0194,-164.6085 659.3798,-144.7622 716.4501,-131.222"/> +<polygon fill="#000000" stroke="#000000" points="717.1287,-132.8603 721.596,-130.0133 716.3283,-129.4531 717.1287,-132.8603"/> +</g> +<!-- os/exec --> +<g id="node21" class="node"> +<title>os/exec</title> +<g id="a_node21"><a xlink:href="https://godoc.org/os/exec" xlink:title="os/exec" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1167.5,-130C1167.5,-130 1134.5,-130 1134.5,-130 1128.5,-130 1122.5,-124 1122.5,-118 1122.5,-118 1122.5,-106 1122.5,-106 1122.5,-100 1128.5,-94 1134.5,-94 1134.5,-94 1167.5,-94 1167.5,-94 1173.5,-94 1179.5,-100 1179.5,-106 1179.5,-106 1179.5,-118 1179.5,-118 1179.5,-124 1173.5,-130 1167.5,-130"/> +<text text-anchor="middle" x="1151" y="-108.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/exec</text> +</a> +</g> +</g> +<!-- github.com/docker/cli/cli/config/credentials->os/exec --> +<g id="edge25" class="edge"> +<title>github.com/docker/cli/cli/config/credentials->os/exec</title> +<path fill="none" stroke="#000000" d="M832.4024,-281.8347C862.2838,-258.1769 917.424,-216.4865 969,-188 995.6062,-173.3049 1072.8374,-142.4417 1117.6832,-124.9021"/> +<polygon fill="#000000" stroke="#000000" points="1118.4318,-126.4885 1122.4524,-123.0393 1117.1583,-123.2284 1118.4318,-126.4885"/> +</g> +<!-- github.com/docker/docker/pkg/homedir->errors --> +<g id="edge42" class="edge"> +<title>github.com/docker/docker/pkg/homedir->errors</title> +<path fill="none" stroke="#000000" d="M1476.566,-381.2739C1675.9979,-359.5008 2072.4064,-316.2228 2188.4022,-303.5589"/> +<polygon fill="#000000" stroke="#000000" points="2188.9314,-305.2616 2193.7119,-302.9792 2188.5515,-301.7823 2188.9314,-305.2616"/> +</g> +<!-- github.com/docker/docker/pkg/homedir->os --> +<g id="edge43" class="edge"> +<title>github.com/docker/docker/pkg/homedir->os</title> +<path fill="none" stroke="#000000" d="M1381.5627,-375.6808C1404.609,-353.7518 1435.827,-315.753 1420,-282 1364.5306,-163.7047 1314.4518,-144.6161 1194,-94 960.0587,4.3064 877.5008,-69.7646 626,-36 600.3612,-32.5579 571.4609,-27.6887 550.0122,-23.8824"/> +<polygon fill="#000000" stroke="#000000" points="550.2325,-22.1441 545.0028,-22.9887 549.6177,-25.5897 550.2325,-22.1441"/> +</g> +<!-- github.com/docker/docker/pkg/homedir->path/filepath --> +<g id="edge45" class="edge"> +<title>github.com/docker/docker/pkg/homedir->path/filepath</title> +<path fill="none" stroke="#000000" d="M1360.5784,-375.8759C1361.0557,-360.9211 1361.7362,-339.5983 1362.2544,-323.3629"/> +<polygon fill="#000000" stroke="#000000" points="1364.0129,-323.1218 1362.4233,-318.0685 1360.5147,-323.0101 1364.0129,-323.1218"/> +</g> +<!-- github.com/docker/docker/pkg/homedir->strings --> +<g id="edge46" class="edge"> +<title>github.com/docker/docker/pkg/homedir->strings</title> +<path fill="none" stroke="#000000" d="M1404.8867,-375.9352C1429.3442,-363.5353 1457.5068,-344.49 1472,-318 1522.168,-226.3051 1498.622,-94.096 1486.0842,-41.1775"/> +<polygon fill="#000000" stroke="#000000" points="1487.7111,-40.4593 1484.8349,-36.0107 1484.3091,-41.282 1487.7111,-40.4593"/> +</g> +<!-- os/user --> +<g id="node22" class="node"> +<title>os/user</title> +<g id="a_node22"><a xlink:href="https://godoc.org/os/user" xlink:title="os/user" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1596.5,-318C1596.5,-318 1565.5,-318 1565.5,-318 1559.5,-318 1553.5,-312 1553.5,-306 1553.5,-306 1553.5,-294 1553.5,-294 1553.5,-288 1559.5,-282 1565.5,-282 1565.5,-282 1596.5,-282 1596.5,-282 1602.5,-282 1608.5,-288 1608.5,-294 1608.5,-294 1608.5,-306 1608.5,-306 1608.5,-312 1602.5,-318 1596.5,-318"/> +<text text-anchor="middle" x="1581" y="-296.3" font-family="Times,serif" font-size="14.00" fill="#000000">os/user</text> +</a> +</g> +</g> +<!-- github.com/docker/docker/pkg/homedir->os/user --> +<g id="edge44" class="edge"> +<title>github.com/docker/docker/pkg/homedir->os/user</title> +<path fill="none" stroke="#000000" d="M1402.3495,-375.9871C1444.9313,-357.8754 1509.6649,-330.3416 1548.4022,-313.8651"/> +<polygon fill="#000000" stroke="#000000" points="1549.5422,-315.282 1553.4583,-311.7145 1548.1723,-312.0612 1549.5422,-315.282"/> +</g> +<!-- github.com/pkg/errors->fmt --> +<g id="edge81" class="edge"> +<title>github.com/pkg/errors->fmt</title> +<path fill="none" stroke="#000000" d="M1713.129,-93.9871C1780.4283,-74.1559 1886.0616,-43.0289 1938.798,-27.489"/> +<polygon fill="#000000" stroke="#000000" points="1939.4319,-29.1267 1943.7333,-26.0347 1938.4425,-25.7694 1939.4319,-29.1267"/> +</g> +<!-- github.com/pkg/errors->io --> +<g id="edge82" class="edge"> +<title>github.com/pkg/errors->io</title> +<path fill="none" stroke="#000000" d="M1582.7503,-101.7165C1446.7594,-81.5219 1149.4983,-37.3789 1051.2413,-22.7878"/> +<polygon fill="#000000" stroke="#000000" points="1051.3631,-21.0368 1046.1602,-22.0333 1050.8489,-24.4988 1051.3631,-21.0368"/> +</g> +<!-- github.com/pkg/errors->strings --> +<g id="edge85" class="edge"> +<title>github.com/pkg/errors->strings</title> +<path fill="none" stroke="#000000" d="M1618.8367,-93.8759C1587.9801,-77.0124 1542.3072,-52.0516 1511.8726,-35.4187"/> +<polygon fill="#000000" stroke="#000000" points="1512.3502,-33.6855 1507.1234,-32.8233 1510.6717,-36.7568 1512.3502,-33.6855"/> +</g> +<!-- path --> +<g id="node38" class="node"> +<title>path</title> +<g id="a_node38"><a xlink:href="https://godoc.org/path" xlink:title="path" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1624,-36C1624,-36 1594,-36 1594,-36 1588,-36 1582,-30 1582,-24 1582,-24 1582,-12 1582,-12 1582,-6 1588,0 1594,0 1594,0 1624,0 1624,0 1630,0 1636,-6 1636,-12 1636,-12 1636,-24 1636,-24 1636,-30 1630,-36 1624,-36"/> +<text text-anchor="middle" x="1609" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">path</text> +</a> +</g> +</g> +<!-- github.com/pkg/errors->path --> +<g id="edge83" class="edge"> +<title>github.com/pkg/errors->path</title> +<path fill="none" stroke="#000000" d="M1643.7092,-93.8759C1636.8087,-78.7911 1626.9443,-57.227 1619.4941,-40.9405"/> +<polygon fill="#000000" stroke="#000000" points="1620.9368,-39.8874 1617.2654,-36.0685 1617.754,-41.3434 1620.9368,-39.8874"/> +</g> +<!-- runtime --> +<g id="node39" class="node"> +<title>runtime</title> +<g id="a_node39"><a xlink:href="https://godoc.org/runtime" xlink:title="runtime" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1712.5,-36C1712.5,-36 1677.5,-36 1677.5,-36 1671.5,-36 1665.5,-30 1665.5,-24 1665.5,-24 1665.5,-12 1665.5,-12 1665.5,-6 1671.5,0 1677.5,0 1677.5,0 1712.5,0 1712.5,0 1718.5,0 1724.5,-6 1724.5,-12 1724.5,-12 1724.5,-24 1724.5,-24 1724.5,-30 1718.5,-36 1712.5,-36"/> +<text text-anchor="middle" x="1695" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">runtime</text> +</a> +</g> +</g> +<!-- github.com/pkg/errors->runtime --> +<g id="edge84" class="edge"> +<title>github.com/pkg/errors->runtime</title> +<path fill="none" stroke="#000000" d="M1660.2908,-93.8759C1667.1913,-78.7911 1677.0557,-57.227 1684.5059,-40.9405"/> +<polygon fill="#000000" stroke="#000000" points="1686.246,-41.3434 1686.7346,-36.0685 1683.0632,-39.8874 1686.246,-41.3434"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->bytes --> +<g id="edge27" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->bytes</title> +<path fill="none" stroke="#000000" d="M699.695,-187.9234C676.7845,-175.2795 650.1209,-155.9875 637,-130 622.3372,-100.9586 638.4454,-64.0663 652.3779,-40.7356"/> +<polygon fill="#000000" stroke="#000000" points="654.0027,-41.4337 655.1256,-36.2567 651.0193,-39.6035 654.0027,-41.4337"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->encoding/json --> +<g id="edge28" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->encoding/json</title> +<path fill="none" stroke="#000000" d="M687.6472,-187.9738C582.9444,-151.9222 348.9088,-71.3386 245.18,-35.6224"/> +<polygon fill="#000000" stroke="#000000" points="245.6374,-33.9291 240.34,-33.9559 244.4979,-37.2385 245.6374,-33.9291"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->fmt --> +<g id="edge29" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->fmt</title> +<path fill="none" stroke="#000000" d="M887.6731,-196.1443C1154.8686,-178.1123 1694.8018,-140.6793 1735,-130 1812.5486,-109.3979 1895.6292,-63.7777 1939.5557,-37.56"/> +<polygon fill="#000000" stroke="#000000" points="1940.5536,-39.0022 1943.9411,-34.9293 1938.7531,-36.0008 1940.5536,-39.0022"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->io --> +<g id="edge31" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->io</title> +<path fill="none" stroke="#000000" d="M830.8914,-187.9399C892.5304,-173.939 966.7036,-153.0798 989,-130 1012.1116,-106.0764 1017.8863,-66.3718 1019.0761,-41.2273"/> +<polygon fill="#000000" stroke="#000000" points="1020.8298,-41.1546 1019.2649,-36.0936 1017.3322,-41.0259 1020.8298,-41.1546"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->os --> +<g id="edge32" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->os</title> +<path fill="none" stroke="#000000" d="M708.4095,-187.7767C684.5249,-173.3822 651.4436,-152.0975 625,-130 591.2531,-101.7995 556.9566,-63.9225 536.5846,-40.2407"/> +<polygon fill="#000000" stroke="#000000" points="537.7728,-38.9377 533.1916,-36.2778 535.1142,-41.2141 537.7728,-38.9377"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->strings --> +<g id="edge34" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->strings</title> +<path fill="none" stroke="#000000" d="M818.0386,-187.936C875.1239,-174.007 954.12,-153.2389 1022,-130 1061.2022,-116.579 1068.4023,-106.205 1108,-94 1230.8475,-56.1355 1382.4067,-31.8561 1447.6423,-22.4374"/> +<polygon fill="#000000" stroke="#000000" points="1448.2359,-24.1202 1452.9369,-21.6783 1447.7391,-20.6557 1448.2359,-24.1202"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->github.com/docker/docker-credential-helpers/credentials --> +<g id="edge30" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->github.com/docker/docker-credential-helpers/credentials</title> +<path fill="none" stroke="#000000" d="M754.0751,-187.8759C765.9919,-172.531 783.1154,-150.4815 795.835,-134.1029"/> +<polygon fill="#000000" stroke="#000000" points="797.2834,-135.0909 798.9681,-130.0685 794.5191,-132.9442 797.2834,-135.0909"/> +</g> +<!-- github.com/docker/docker-credential-helpers/client->os/exec --> +<g id="edge33" class="edge"> +<title>github.com/docker/docker-credential-helpers/client->os/exec</title> +<path fill="none" stroke="#000000" d="M842.8954,-187.9971C915.3721,-174.4141 1014.3608,-154.0405 1100,-130 1105.7206,-128.3941 1111.7294,-126.4823 1117.5343,-124.5139"/> +<polygon fill="#000000" stroke="#000000" points="1118.1953,-126.1371 1122.3516,-122.8526 1117.0542,-122.8283 1118.1953,-126.1371"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->bufio --> +<g id="edge35" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->bufio</title> +<path fill="none" stroke="#000000" d="M813,-93.8759C813,-78.9211 813,-57.5983 813,-41.3629"/> +<polygon fill="#000000" stroke="#000000" points="814.7501,-41.0685 813,-36.0685 811.2501,-41.0685 814.7501,-41.0685"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->bytes --> +<g id="edge36" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->bytes</title> +<path fill="none" stroke="#000000" d="M785.0425,-93.8759C760.6149,-78.04 725.1726,-55.0636 699.6971,-38.5485"/> +<polygon fill="#000000" stroke="#000000" points="700.5274,-37.0012 695.3799,-35.7497 698.6235,-39.9381 700.5274,-37.0012"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->encoding/json --> +<g id="edge37" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->encoding/json</title> +<path fill="none" stroke="#000000" d="M694.383,-93.9871C558.8459,-73.4047 343.1816,-40.6544 245.3082,-25.7915"/> +<polygon fill="#000000" stroke="#000000" points="245.3749,-24.0317 240.1688,-25.0111 244.8494,-27.492 245.3749,-24.0317"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->fmt --> +<g id="edge38" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->fmt</title> +<path fill="none" stroke="#000000" d="M974.6242,-98.8803C1251.5666,-76.3996 1800.4046,-31.848 1938.7538,-20.6176"/> +<polygon fill="#000000" stroke="#000000" points="1939.0437,-22.3499 1943.8857,-20.201 1938.7605,-18.8614 1939.0437,-22.3499"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->io --> +<g id="edge39" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->io</title> +<path fill="none" stroke="#000000" d="M852.7189,-93.8759C891.8813,-76.0056 950.9711,-49.0423 987.1527,-32.5322"/> +<polygon fill="#000000" stroke="#000000" points="988.0615,-34.0412 991.8838,-30.3734 986.6085,-30.8571 988.0615,-34.0412"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->os --> +<g id="edge40" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->os</title> +<path fill="none" stroke="#000000" d="M756.4701,-93.9871C695.1682,-74.4536 599.4727,-43.9608 550.0082,-28.1992"/> +<polygon fill="#000000" stroke="#000000" points="550.3741,-26.4792 545.0787,-26.6285 549.3114,-29.814 550.3741,-26.4792"/> +</g> +<!-- github.com/docker/docker-credential-helpers/credentials->strings --> +<g id="edge41" class="edge"> +<title>github.com/docker/docker-credential-helpers/credentials->strings</title> +<path fill="none" stroke="#000000" d="M940.8151,-93.9871C1098.6257,-71.7469 1357.2455,-35.2997 1447.6149,-22.564"/> +<polygon fill="#000000" stroke="#000000" points="1448.1062,-24.2622 1452.8131,-21.8314 1447.6177,-20.7964 1448.1062,-24.2622"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn --> +<g id="node23" class="node"> +<title>github.com/google/go-containerregistry/pkg/authn</title> +<g id="a_node23"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/authn" xlink:title="github.com/google/go-containerregistry/pkg/authn" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1303,-600C1303,-600 1037,-600 1037,-600 1031,-600 1025,-594 1025,-588 1025,-588 1025,-576 1025,-576 1025,-570 1031,-564 1037,-564 1037,-564 1303,-564 1303,-564 1309,-564 1315,-570 1315,-576 1315,-576 1315,-588 1315,-588 1315,-594 1309,-600 1303,-600"/> +<text text-anchor="middle" x="1170" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/authn</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->encoding/json --> +<g id="edge47" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->encoding/json</title> +<path fill="none" stroke="#000000" d="M1024.9508,-574.5193C829.7314,-563.3542 493.2775,-539.9443 375,-506 213.8533,-459.7527 59,-467.6516 59,-300 59,-300 59,-300 59,-206 59,-134.2762 124.6313,-71.1736 164.3711,-39.539"/> +<polygon fill="#000000" stroke="#000000" points="165.759,-40.6738 168.6075,-36.2074 163.5955,-37.9226 165.759,-40.6738"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->github.com/docker/cli/cli/config --> +<g id="edge48" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->github.com/docker/cli/cli/config</title> +<path fill="none" stroke="#000000" d="M1170,-563.8759C1170,-548.9211 1170,-527.5983 1170,-511.3629"/> +<polygon fill="#000000" stroke="#000000" points="1171.7501,-511.0685 1170,-506.0685 1168.2501,-511.0685 1171.7501,-511.0685"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->github.com/docker/cli/cli/config/types --> +<g id="edge49" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->github.com/docker/cli/cli/config/types</title> +<path fill="none" stroke="#000000" d="M1024.8472,-570.6895C771.6299,-545.7577 291.601,-472.3501 462,-282 480.6886,-261.1233 838.8562,-230.1019 1029.6383,-214.9633"/> +<polygon fill="#000000" stroke="#000000" points="1029.8273,-216.7039 1034.6735,-214.5644 1029.5508,-213.2148 1029.8273,-216.7039"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->os --> +<g id="edge52" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->os</title> +<path fill="none" stroke="#000000" d="M1024.6507,-570.094C867.0858,-556.2002 626.3053,-531.8169 538,-506 353.3099,-452.0042 250.3428,-480.9476 148,-318 53.2875,-167.2011 379.6896,-57.8331 485.7838,-26.8904"/> +<polygon fill="#000000" stroke="#000000" points="486.351,-28.5482 490.6669,-25.4764 485.3774,-25.1863 486.351,-28.5482"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/logs --> +<g id="node24" class="node"> +<title>github.com/google/go-containerregistry/pkg/logs</title> +<g id="a_node24"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/logs" xlink:title="github.com/google/go-containerregistry/pkg/logs" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M1630.5,-506C1630.5,-506 1371.5,-506 1371.5,-506 1365.5,-506 1359.5,-500 1359.5,-494 1359.5,-494 1359.5,-482 1359.5,-482 1359.5,-476 1365.5,-470 1371.5,-470 1371.5,-470 1630.5,-470 1630.5,-470 1636.5,-470 1642.5,-476 1642.5,-482 1642.5,-482 1642.5,-494 1642.5,-494 1642.5,-500 1636.5,-506 1630.5,-506"/> +<text text-anchor="middle" x="1501" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/logs</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->github.com/google/go-containerregistry/pkg/logs --> +<g id="edge50" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->github.com/google/go-containerregistry/pkg/logs</title> +<path fill="none" stroke="#000000" d="M1233.4285,-563.9871C1290.4862,-547.7834 1374.0994,-524.0382 1432.629,-507.4165"/> +<polygon fill="#000000" stroke="#000000" points="1433.1729,-509.0813 1437.5046,-506.0319 1432.2167,-505.7145 1433.1729,-509.0813"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/name --> +<g id="node25" class="node"> +<title>github.com/google/go-containerregistry/pkg/name</title> +<g id="a_node25"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/name" xlink:title="github.com/google/go-containerregistry/pkg/name" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2106,-506C2106,-506 1840,-506 1840,-506 1834,-506 1828,-500 1828,-494 1828,-494 1828,-482 1828,-482 1828,-476 1834,-470 1840,-470 1840,-470 2106,-470 2106,-470 2112,-470 2118,-476 2118,-482 2118,-482 2118,-494 2118,-494 2118,-500 2112,-506 2106,-506"/> +<text text-anchor="middle" x="1973" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/name</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/authn->github.com/google/go-containerregistry/pkg/name --> +<g id="edge51" class="edge"> +<title>github.com/google/go-containerregistry/pkg/authn->github.com/google/go-containerregistry/pkg/name</title> +<path fill="none" stroke="#000000" d="M1315.4172,-564.9773C1459.019,-548.1671 1677.6313,-522.5762 1822.8531,-505.5764"/> +<polygon fill="#000000" stroke="#000000" points="1823.2253,-507.2948 1827.9879,-504.9753 1822.8183,-503.8186 1823.2253,-507.2948"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/logs->io/ioutil --> +<g id="edge59" class="edge"> +<title>github.com/google/go-containerregistry/pkg/logs->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1359.3559,-475.5064C1193.7666,-460.2638 930.0058,-433.8275 833,-412 723.3831,-387.3348 599.1886,-338.8368 540.3782,-314.5625"/> +<polygon fill="#000000" stroke="#000000" points="540.8642,-312.8698 535.575,-312.5738 539.5252,-316.1035 540.8642,-312.8698"/> +</g> +<!-- log --> +<g id="node30" class="node"> +<title>log</title> +<g id="a_node30"><a xlink:href="https://godoc.org/log" xlink:title="log" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1725,-412C1725,-412 1695,-412 1695,-412 1689,-412 1683,-406 1683,-400 1683,-400 1683,-388 1683,-388 1683,-382 1689,-376 1695,-376 1695,-376 1725,-376 1725,-376 1731,-376 1737,-382 1737,-388 1737,-388 1737,-400 1737,-400 1737,-406 1731,-412 1725,-412"/> +<text text-anchor="middle" x="1710" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">log</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/logs->log --> +<g id="edge60" class="edge"> +<title>github.com/google/go-containerregistry/pkg/logs->log</title> +<path fill="none" stroke="#000000" d="M1541.2973,-469.8759C1581.1436,-451.9546 1641.3231,-424.8882 1678.003,-408.391"/> +<polygon fill="#000000" stroke="#000000" points="1678.9553,-409.8816 1682.7975,-406.2346 1677.5196,-406.6896 1678.9553,-409.8816"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->fmt --> +<g id="edge61" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->fmt</title> +<path fill="none" stroke="#000000" d="M1958.49,-469.9227C1947.7181,-455.2755 1933.9131,-433.5925 1928,-412 1923.774,-396.5682 1925.0684,-391.7291 1928,-376 1938.7373,-318.3907 2012.2627,-187.6093 2023,-130 2025.9316,-114.2709 2027.7287,-109.2853 2023,-94 2016.7808,-73.8967 2003.2102,-54.3746 1991.4918,-40.1506"/> +<polygon fill="#000000" stroke="#000000" points="1992.7993,-38.9863 1988.2434,-36.2831 1990.1192,-41.2374 1992.7993,-38.9863"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->strings --> +<g id="edge65" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->strings</title> +<path fill="none" stroke="#000000" d="M1958.2351,-469.7202C1946.6186,-454.7857 1930.5239,-432.8163 1919,-412 1844.8842,-278.1204 1903.0477,-193.7487 1787,-94 1776.4275,-84.9124 1589.3534,-42.3984 1512.5268,-25.2242"/> +<polygon fill="#000000" stroke="#000000" points="1512.451,-23.4142 1507.1897,-24.0322 1511.688,-26.83 1512.451,-23.4142"/> +</g> +<!-- net --> +<g id="node31" class="node"> +<title>net</title> +<g id="a_node31"><a xlink:href="https://godoc.org/net" xlink:title="net" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2183,-412C2183,-412 2153,-412 2153,-412 2147,-412 2141,-406 2141,-400 2141,-400 2141,-388 2141,-388 2141,-382 2147,-376 2153,-376 2153,-376 2183,-376 2183,-376 2189,-376 2195,-382 2195,-388 2195,-388 2195,-400 2195,-400 2195,-406 2189,-412 2183,-412"/> +<text text-anchor="middle" x="2168" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->net --> +<g id="edge62" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->net</title> +<path fill="none" stroke="#000000" d="M2010.598,-469.8759C2046.9905,-452.3328 2101.5618,-426.0266 2135.9505,-409.4495"/> +<polygon fill="#000000" stroke="#000000" points="2137.1292,-410.8241 2140.8733,-407.0765 2135.6093,-407.6713 2137.1292,-410.8241"/> +</g> +<!-- net/url --> +<g id="node32" class="node"> +<title>net/url</title> +<g id="a_node32"><a xlink:href="https://godoc.org/net/url" xlink:title="net/url" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2266,-412C2266,-412 2236,-412 2236,-412 2230,-412 2224,-406 2224,-400 2224,-400 2224,-388 2224,-388 2224,-382 2230,-376 2236,-376 2236,-376 2266,-376 2266,-376 2272,-376 2278,-382 2278,-388 2278,-388 2278,-400 2278,-400 2278,-406 2272,-412 2266,-412"/> +<text text-anchor="middle" x="2251" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/url</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->net/url --> +<g id="edge63" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->net/url</title> +<path fill="none" stroke="#000000" d="M2034.6626,-469.868C2082.5982,-455.3586 2150.5476,-433.8829 2209,-412 2212.1204,-410.8318 2215.3474,-409.5624 2218.5621,-408.2565"/> +<polygon fill="#000000" stroke="#000000" points="2219.6318,-409.7083 2223.5864,-406.1836 2218.2969,-406.4729 2219.6318,-409.7083"/> +</g> +<!-- regexp --> +<g id="node33" class="node"> +<title>regexp</title> +<g id="a_node33"><a xlink:href="https://godoc.org/regexp" xlink:title="regexp" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M1985,-412C1985,-412 1955,-412 1955,-412 1949,-412 1943,-406 1943,-400 1943,-400 1943,-388 1943,-388 1943,-382 1949,-376 1955,-376 1955,-376 1985,-376 1985,-376 1991,-376 1997,-382 1997,-388 1997,-388 1997,-400 1997,-400 1997,-406 1991,-412 1985,-412"/> +<text text-anchor="middle" x="1970" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">regexp</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->regexp --> +<g id="edge64" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->regexp</title> +<path fill="none" stroke="#000000" d="M1972.4216,-469.8759C1971.9443,-454.9211 1971.2638,-433.5983 1970.7456,-417.3629"/> +<polygon fill="#000000" stroke="#000000" points="1972.4853,-417.0101 1970.5767,-412.0685 1968.9871,-417.1218 1972.4853,-417.0101"/> +</g> +<!-- unicode/utf8 --> +<g id="node34" class="node"> +<title>unicode/utf8</title> +<g id="a_node34"><a xlink:href="https://godoc.org/unicode/utf8" xlink:title="unicode/utf8" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2099.5,-412C2099.5,-412 2038.5,-412 2038.5,-412 2032.5,-412 2026.5,-406 2026.5,-400 2026.5,-400 2026.5,-388 2026.5,-388 2026.5,-382 2032.5,-376 2038.5,-376 2038.5,-376 2099.5,-376 2099.5,-376 2105.5,-376 2111.5,-382 2111.5,-388 2111.5,-388 2111.5,-400 2111.5,-400 2111.5,-406 2105.5,-412 2099.5,-412"/> +<text text-anchor="middle" x="2069" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">unicode/utf8</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/name->unicode/utf8 --> +<g id="edge66" class="edge"> +<title>github.com/google/go-containerregistry/pkg/name->unicode/utf8</title> +<path fill="none" stroke="#000000" d="M1991.5098,-469.8759C2007.3139,-454.401 2030.0819,-432.1073 2046.8509,-415.6877"/> +<polygon fill="#000000" stroke="#000000" points="2048.1988,-416.8171 2050.5471,-412.0685 2045.7501,-414.3163 2048.1988,-416.8171"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry --> +<g id="node26" class="node"> +<title>github.com/google/go-containerregistry/pkg/internal/retry</title> +<g id="a_node26"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry" xlink:title="github.com/google/go-containerregistry/pkg/internal/retry" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2744.5,-600C2744.5,-600 2437.5,-600 2437.5,-600 2431.5,-600 2425.5,-594 2425.5,-588 2425.5,-588 2425.5,-576 2425.5,-576 2425.5,-570 2431.5,-564 2437.5,-564 2437.5,-564 2744.5,-564 2744.5,-564 2750.5,-564 2756.5,-570 2756.5,-576 2756.5,-576 2756.5,-588 2756.5,-588 2756.5,-594 2750.5,-600 2744.5,-600"/> +<text text-anchor="middle" x="2591" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/internal/retry</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry->context --> +<g id="edge53" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry->context</title> +<path fill="none" stroke="#000000" d="M2575.768,-563.8759C2562.8718,-548.531 2544.3408,-526.4815 2530.5758,-510.1029"/> +<polygon fill="#000000" stroke="#000000" points="2531.7419,-508.7703 2527.1852,-506.0685 2529.0625,-511.0222 2531.7419,-508.7703"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry->fmt --> +<g id="edge54" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry->fmt</title> +<path fill="none" stroke="#000000" d="M2546.1332,-563.8924C2520.2926,-551.2872 2489.2249,-532.0465 2470,-506 2439.3971,-464.5384 2440,-445.5326 2440,-394 2440,-394 2440,-394 2440,-206 2440,-153.5763 2442.6557,-128.2881 2403,-94 2342.8493,-41.9909 2093.343,-24.26 2003.4565,-19.4922"/> +<polygon fill="#000000" stroke="#000000" points="2003.3619,-17.735 1998.2776,-19.2226 2003.1798,-21.2303 2003.3619,-17.735"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry/wait --> +<g id="node27" class="node"> +<title>github.com/google/go-containerregistry/pkg/internal/retry/wait</title> +<g id="a_node27"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/internal/retry/wait" xlink:title="github.com/google/go-containerregistry/pkg/internal/retry/wait" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2915,-506C2915,-506 2581,-506 2581,-506 2575,-506 2569,-500 2569,-494 2569,-494 2569,-482 2569,-482 2569,-476 2575,-470 2581,-470 2581,-470 2915,-470 2915,-470 2921,-470 2927,-476 2927,-482 2927,-482 2927,-494 2927,-494 2927,-500 2921,-506 2915,-506"/> +<text text-anchor="middle" x="2748" y="-484.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/internal/retry/wait</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry->github.com/google/go-containerregistry/pkg/internal/retry/wait --> +<g id="edge55" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry->github.com/google/go-containerregistry/pkg/internal/retry/wait</title> +<path fill="none" stroke="#000000" d="M2621.2712,-563.8759C2647.6605,-548.0759 2685.9222,-525.1676 2713.4922,-508.6607"/> +<polygon fill="#000000" stroke="#000000" points="2714.4308,-510.1385 2717.8218,-506.0685 2712.6329,-507.1356 2714.4308,-510.1385"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry/wait->errors --> +<g id="edge56" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry/wait->errors</title> +<path fill="none" stroke="#000000" d="M2697.469,-469.9738C2590.0101,-431.6393 2341.4084,-342.954 2252.9554,-311.3997"/> +<polygon fill="#000000" stroke="#000000" points="2253.4098,-309.7038 2248.1125,-309.672 2252.2338,-313.0003 2253.4098,-309.7038"/> +</g> +<!-- math/rand --> +<g id="node28" class="node"> +<title>math/rand</title> +<g id="a_node28"><a xlink:href="https://godoc.org/math/rand" xlink:title="math/rand" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2772,-412C2772,-412 2724,-412 2724,-412 2718,-412 2712,-406 2712,-400 2712,-400 2712,-388 2712,-388 2712,-382 2718,-376 2724,-376 2724,-376 2772,-376 2772,-376 2778,-376 2784,-382 2784,-388 2784,-388 2784,-400 2784,-400 2784,-406 2778,-412 2772,-412"/> +<text text-anchor="middle" x="2748" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">math/rand</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry/wait->math/rand --> +<g id="edge57" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry/wait->math/rand</title> +<path fill="none" stroke="#000000" d="M2748,-469.8759C2748,-454.9211 2748,-433.5983 2748,-417.3629"/> +<polygon fill="#000000" stroke="#000000" points="2749.7501,-417.0685 2748,-412.0685 2746.2501,-417.0685 2749.7501,-417.0685"/> +</g> +<!-- time --> +<g id="node29" class="node"> +<title>time</title> +<g id="a_node29"><a xlink:href="https://godoc.org/time" xlink:title="time" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M2921,-412C2921,-412 2891,-412 2891,-412 2885,-412 2879,-406 2879,-400 2879,-400 2879,-388 2879,-388 2879,-382 2885,-376 2891,-376 2891,-376 2921,-376 2921,-376 2927,-376 2933,-382 2933,-388 2933,-388 2933,-400 2933,-400 2933,-406 2927,-412 2921,-412"/> +<text text-anchor="middle" x="2906" y="-390.3" font-family="Times,serif" font-size="14.00" fill="#000000">time</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/internal/retry/wait->time --> +<g id="edge58" class="edge"> +<title>github.com/google/go-containerregistry/pkg/internal/retry/wait->time</title> +<path fill="none" stroke="#000000" d="M2778.464,-469.8759C2806.0429,-453.4682 2846.5064,-429.3949 2874.4248,-412.7853"/> +<polygon fill="#000000" stroke="#000000" points="2875.3916,-414.2464 2878.7939,-410.1859 2873.6021,-411.2385 2875.3916,-414.2464"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport --> +<g id="node35" class="node"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport</title> +<g id="a_node35"><a xlink:href="https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport" xlink:title="github.com/google/go-containerregistry/pkg/v1/remote/transport" target="_blank"> +<path fill="#afeeee" stroke="#afeeee" d="M2288,-694C2288,-694 1944,-694 1944,-694 1938,-694 1932,-688 1932,-682 1932,-682 1932,-670 1932,-670 1932,-664 1938,-658 1944,-658 1944,-658 2288,-658 2288,-658 2294,-658 2300,-664 2300,-670 2300,-670 2300,-682 2300,-682 2300,-688 2294,-694 2288,-694"/> +<text text-anchor="middle" x="2116" y="-672.3" font-family="Times,serif" font-size="14.00" fill="#000000">github.com/google/go-containerregistry/pkg/v1/remote/transport</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->encoding/base64 --> +<g id="edge67" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->encoding/base64</title> +<path fill="none" stroke="#000000" d="M1931.9625,-668.5155C1644.7457,-656.1342 1103.0357,-629.7791 912,-600 742.6532,-573.6018 695.3448,-573.9546 538,-506 419.5411,-454.8395 295.2762,-362.5606 242.3584,-321.1352"/> +<polygon fill="#000000" stroke="#000000" points="243.3825,-319.7143 238.3691,-318.0027 241.2209,-322.4671 243.3825,-319.7143"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->encoding/json --> +<g id="edge68" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->encoding/json</title> +<path fill="none" stroke="#000000" d="M1931.9205,-669.2048C1417.1107,-648.9922 0,-584.5816 0,-488 0,-488 0,-488 0,-206 0,-124.3433 89.7319,-66.7993 147.1424,-38.3391"/> +<polygon fill="#000000" stroke="#000000" points="148.1834,-39.778 151.9049,-36.0079 146.6446,-36.6343 148.1834,-39.778"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->fmt --> +<g id="edge69" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->fmt</title> +<path fill="none" stroke="#000000" d="M2189.4654,-657.8977C2267.8907,-633.6575 2381,-582.3435 2381,-488 2381,-488 2381,-488 2381,-206 2381,-123.0113 2099.9384,-48.5681 2003.2301,-25.4098"/> +<polygon fill="#000000" stroke="#000000" points="2003.4882,-23.6724 1998.2188,-24.2162 2002.6772,-27.0771 2003.4882,-23.6724"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->strings --> +<g id="edge79" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->strings</title> +<path fill="none" stroke="#000000" d="M2051.971,-657.9892C1981.8298,-634.7862 1871.5349,-587.4217 1813,-506 1704.2154,-354.6813 1859.8518,-232.36 1735,-94 1719.7682,-77.1202 1577.1558,-41.2558 1512.0894,-25.5889"/> +<polygon fill="#000000" stroke="#000000" points="1512.436,-23.8725 1507.1656,-24.4063 1511.6186,-27.2757 1512.436,-23.8725"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->io/ioutil --> +<g id="edge74" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->io/ioutil</title> +<path fill="none" stroke="#000000" d="M1931.874,-673.6681C1662.9766,-668.5124 1176.7194,-652.0769 1010,-600 808.4444,-537.0416 602.7197,-379.233 531.8324,-321.5489"/> +<polygon fill="#000000" stroke="#000000" points="532.7377,-320.0288 527.7578,-318.2224 530.5243,-322.74 532.7377,-320.0288"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/authn --> +<g id="edge70" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/authn</title> +<path fill="none" stroke="#000000" d="M1934.721,-657.9871C1756.3941,-640.2675 1487.3067,-613.5294 1320.4603,-596.9506"/> +<polygon fill="#000000" stroke="#000000" points="1320.21,-595.1672 1315.0614,-596.4141 1319.8639,-598.65 1320.21,-595.1672"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/logs --> +<g id="edge72" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/logs</title> +<path fill="none" stroke="#000000" d="M2003.1445,-657.9396C1932.1439,-645.1194 1839.1201,-625.6344 1759,-600 1679.4792,-574.5574 1591.0895,-533.1381 1541.1524,-508.434"/> +<polygon fill="#000000" stroke="#000000" points="1541.6983,-506.7513 1536.4414,-506.0966 1540.1426,-509.8866 1541.6983,-506.7513"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/name --> +<g id="edge73" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/name</title> +<path fill="none" stroke="#000000" d="M2102.2886,-657.9738C2076.0903,-623.5313 2018.9753,-548.443 1990.2397,-510.6648"/> +<polygon fill="#000000" stroke="#000000" points="1991.3,-509.168 1986.88,-506.2479 1988.5142,-511.287 1991.3,-509.168"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/internal/retry --> +<g id="edge71" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->github.com/google/go-containerregistry/pkg/internal/retry</title> +<path fill="none" stroke="#000000" d="M2207.0228,-657.9871C2289.5608,-641.6532 2410.8219,-617.6563 2494.9015,-601.0174"/> +<polygon fill="#000000" stroke="#000000" points="2495.3162,-602.7194 2499.8813,-600.0319 2494.6366,-599.2859 2495.3162,-602.7194"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->time --> +<g id="edge80" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->time</title> +<path fill="none" stroke="#000000" d="M2300.3792,-662.575C2462.7573,-649.2977 2686.2105,-627.0731 2771,-600 2853.6167,-573.6206 2897.8516,-580.6478 2942,-506 2958.933,-477.369 2939.7507,-439.8373 2923.5517,-416.3178"/> +<polygon fill="#000000" stroke="#000000" points="2924.884,-415.1705 2920.5711,-412.0945 2922.0245,-417.1887 2924.884,-415.1705"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->net --> +<g id="edge75" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->net</title> +<path fill="none" stroke="#000000" d="M2119.3319,-657.9306C2128.409,-608.7051 2153.5706,-472.252 2163.6966,-417.3379"/> +<polygon fill="#000000" stroke="#000000" points="2165.4427,-417.5183 2164.6285,-412.2838 2162.0007,-416.8836 2165.4427,-417.5183"/> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->net/url --> +<g id="edge78" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->net/url</title> +<path fill="none" stroke="#000000" d="M2125.971,-657.7097C2134.2333,-642.401 2146.1711,-619.9239 2156,-600 2188.4333,-534.2549 2223.9276,-455.2061 2240.8839,-416.96"/> +<polygon fill="#000000" stroke="#000000" points="2242.6293,-417.3406 2243.0537,-412.0601 2239.429,-415.9233 2242.6293,-417.3406"/> +</g> +<!-- net/http --> +<g id="node36" class="node"> +<title>net/http</title> +<g id="a_node36"><a xlink:href="https://godoc.org/net/http" xlink:title="net/http" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3061,-600C3061,-600 3027,-600 3027,-600 3021,-600 3015,-594 3015,-588 3015,-588 3015,-576 3015,-576 3015,-570 3021,-564 3027,-564 3027,-564 3061,-564 3061,-564 3067,-564 3073,-570 3073,-576 3073,-576 3073,-588 3073,-588 3073,-594 3067,-600 3061,-600"/> +<text text-anchor="middle" x="3044" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->net/http --> +<g id="edge76" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->net/http</title> +<path fill="none" stroke="#000000" d="M2300.0852,-665.5289C2517.3945,-652.2055 2865.3751,-627.5852 2993,-600 2998.551,-598.8002 3004.3258,-597.1625 3009.909,-595.3609"/> +<polygon fill="#000000" stroke="#000000" points="3010.7837,-596.9137 3014.9705,-593.6681 3009.6736,-593.5944 3010.7837,-596.9137"/> +</g> +<!-- net/http/httputil --> +<g id="node37" class="node"> +<title>net/http/httputil</title> +<g id="a_node37"><a xlink:href="https://godoc.org/net/http/httputil" xlink:title="net/http/httputil" target="_blank"> +<path fill="#98fb98" stroke="#98fb98" d="M3191.5,-600C3191.5,-600 3114.5,-600 3114.5,-600 3108.5,-600 3102.5,-594 3102.5,-588 3102.5,-588 3102.5,-576 3102.5,-576 3102.5,-570 3108.5,-564 3114.5,-564 3114.5,-564 3191.5,-564 3191.5,-564 3197.5,-564 3203.5,-570 3203.5,-576 3203.5,-576 3203.5,-588 3203.5,-588 3203.5,-594 3197.5,-600 3191.5,-600"/> +<text text-anchor="middle" x="3153" y="-578.3" font-family="Times,serif" font-size="14.00" fill="#000000">net/http/httputil</text> +</a> +</g> +</g> +<!-- github.com/google/go-containerregistry/pkg/v1/remote/transport->net/http/httputil --> +<g id="edge77" class="edge"> +<title>github.com/google/go-containerregistry/pkg/v1/remote/transport->net/http/httputil</title> +<path fill="none" stroke="#000000" d="M2300.0481,-672.1185C2496.5108,-665.5337 2816.2684,-647.79 3088,-600 3091.1029,-599.4543 3094.2717,-598.8244 3097.458,-598.1342"/> +<polygon fill="#000000" stroke="#000000" points="3097.9804,-599.8104 3102.4728,-597.0032 3097.2103,-596.3962 3097.9804,-599.8104"/> +</g> +</g> +</svg> diff --git a/images/image-anatomy.dot.svg b/images/image-anatomy.dot.svg new file mode 100644 index 0000000..d9fdaa5 --- /dev/null +++ b/images/image-anatomy.dot.svg @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="481pt" height="230pt" + viewBox="0.00 0.00 480.52 230.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 226)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-226 476.5204,-226 476.5204,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_layer1</title> +<polygon fill="none" stroke="#000000" points="362.5204,-115 362.5204,-214 464.5204,-214 464.5204,-115 362.5204,-115"/> +<text text-anchor="middle" x="413.5204" y="-198.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text> +</g> +<g id="clust2" class="cluster"> +<title>cluster_layer2</title> +<polygon fill="none" stroke="#000000" points="362.5204,-8 362.5204,-107 464.5204,-107 464.5204,-8 362.5204,-8"/> +<text text-anchor="middle" x="413.5204" y="-91.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text> +</g> +<!-- tag --> +<g id="node1" class="node"> +<title>tag</title> +<ellipse fill="#000000" stroke="#000000" cx="11.0204" cy="-98" rx="3.5" ry="3.5"/> +</g> +<!-- manifest --> +<g id="node2" class="node"> +<title>manifest</title> +<polygon fill="none" stroke="#000000" points="141.5204,-116 83.5204,-116 83.5204,-80 147.5204,-80 147.5204,-110 141.5204,-116"/> +<polyline fill="none" stroke="#000000" points="141.5204,-116 141.5204,-110 "/> +<polyline fill="none" stroke="#000000" points="147.5204,-110 141.5204,-110 "/> +<text text-anchor="middle" x="115.5204" y="-94.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text> +</g> +<!-- tag->manifest --> +<g id="edge1" class="edge"> +<title>tag:head->manifest</title> +<path fill="none" stroke="#000000" d="M14.9894,-98C24.6237,-98 50.1431,-98 73.0579,-98"/> +<polygon fill="#000000" stroke="#000000" points="73.3366,-101.5001 83.3366,-98 73.3365,-94.5001 73.3366,-101.5001"/> +<text text-anchor="middle" x="49.0204" y="-101.8" font-family="Times,serif" font-size="14.00" fill="#000000">digest</text> +<text text-anchor="middle" x="8.5" y="-114.2722" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text> +</g> +<!-- config --> +<g id="node3" class="node"> +<title>config</title> +<polygon fill="none" stroke="#000000" points="294.5204,-116 246.5204,-116 246.5204,-80 300.5204,-80 300.5204,-110 294.5204,-116"/> +<polyline fill="none" stroke="#000000" points="294.5204,-116 294.5204,-110 "/> +<polyline fill="none" stroke="#000000" points="300.5204,-110 294.5204,-110 "/> +<text text-anchor="middle" x="273.5204" y="-94.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- manifest->config --> +<g id="edge2" class="edge"> +<title>manifest->config</title> +<path fill="none" stroke="#000000" d="M147.8754,-98C173.5398,-98 209.4367,-98 236.2927,-98"/> +<polygon fill="#000000" stroke="#000000" points="236.4645,-101.5001 246.4645,-98 236.4645,-94.5001 236.4645,-101.5001"/> +<text text-anchor="middle" x="194.5204" y="-101.8" font-family="Times,serif" font-size="14.00" fill="#000000">(image id)</text> +</g> +<!-- l1 --> +<g id="node4" class="node"> +<title>l1</title> +<polygon fill="none" stroke="#000000" points="444.5204,-171 441.5204,-175 420.5204,-175 417.5204,-171 382.5204,-171 382.5204,-135 444.5204,-135 444.5204,-171"/> +<text text-anchor="middle" x="413.5204" y="-149.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text> +</g> +<!-- manifest->l1 --> +<g id="edge5" class="edge"> +<title>manifest->l1</title> +<path fill="none" stroke="#000000" d="M147.7297,-111.4179C153.5784,-113.5149 159.6776,-115.4853 165.5204,-117 228.1655,-133.2397 301.7402,-142.7616 352.393,-147.8895"/> +<polygon fill="#000000" stroke="#000000" points="352.2296,-151.3903 362.5245,-148.8876 352.9159,-144.424 352.2296,-151.3903"/> +<text text-anchor="middle" x="273.5204" y="-145.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text> +</g> +<!-- l2 --> +<g id="node5" class="node"> +<title>l2</title> +<polygon fill="none" stroke="#000000" points="444.5204,-64 441.5204,-68 420.5204,-68 417.5204,-64 382.5204,-64 382.5204,-28 444.5204,-28 444.5204,-64"/> +<text text-anchor="middle" x="413.5204" y="-42.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text> +</g> +<!-- manifest->l2 --> +<g id="edge6" class="edge"> +<title>manifest->l2</title> +<path fill="none" stroke="#000000" d="M147.5251,-84.7188C172.6249,-74.9475 208.6871,-62.3115 241.5204,-56 277.7838,-49.0291 319.072,-46.456 352.1051,-45.6602"/> +<polygon fill="#000000" stroke="#000000" points="352.592,-49.1516 362.5232,-45.4609 352.4581,-42.1529 352.592,-49.1516"/> +<text text-anchor="middle" x="273.5204" y="-59.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text> +</g> +<!-- config->l1 --> +<g id="edge3" class="edge"> +<title>config->l1</title> +<path fill="none" stroke="#000000" d="M300.5542,-105.0907C316.4942,-109.5797 336.9243,-115.872 354.5204,-123 360.746,-125.5219 367.2045,-128.4562 373.4651,-131.4831"/> +<polygon fill="#000000" stroke="#000000" points="371.9331,-134.6301 382.4458,-135.9469 375.0488,-128.3617 371.9331,-134.6301"/> +<text text-anchor="middle" x="339.0204" y="-126.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +<!-- config->l2 --> +<g id="edge4" class="edge"> +<title>config->l2</title> +<path fill="none" stroke="#000000" d="M300.8462,-87.8504C321.3169,-80.247 349.6613,-69.7191 372.7994,-61.1249"/> +<polygon fill="#000000" stroke="#000000" points="374.2033,-64.3372 382.3589,-57.5743 371.766,-57.7752 374.2033,-64.3372"/> +<text text-anchor="middle" x="339.0204" y="-81.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +</g> +</svg> diff --git a/images/index-anatomy-strange.dot.svg b/images/index-anatomy-strange.dot.svg new file mode 100644 index 0000000..f698139 --- /dev/null +++ b/images/index-anatomy-strange.dot.svg @@ -0,0 +1,125 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="307pt" height="188pt" + viewBox="0.00 0.00 307.41 188.47" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 184.4722)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-184.4722 303.414,-184.4722 303.414,4 -4,4"/> +<!-- tag --> +<g id="node1" class="node"> +<title>tag</title> +<ellipse fill="#000000" stroke="#000000" cx="25.914" cy="-99" rx="3.5" ry="3.5"/> +</g> +<!-- index --> +<g id="node4" class="node"> +<title>index</title> +<polygon fill="none" stroke="#000000" points="113.414,-117 65.414,-117 65.414,-81 119.414,-81 119.414,-111 113.414,-117"/> +<polyline fill="none" stroke="#000000" points="113.414,-117 113.414,-111 "/> +<polyline fill="none" stroke="#000000" points="119.414,-111 113.414,-111 "/> +<text text-anchor="middle" x="92.414" y="-95.3" font-family="Times,serif" font-size="14.00" fill="#000000">index</text> +</g> +<!-- tag->index --> +<g id="edge1" class="edge"> +<title>tag:head->index</title> +<path fill="none" stroke="#000000" d="M29.4894,-99C34.6004,-99 44.5446,-99 55.0454,-99"/> +<polygon fill="#000000" stroke="#000000" points="55.1441,-102.5001 65.1441,-99 55.144,-95.5001 55.1441,-102.5001"/> +<text text-anchor="middle" x="23" y="-115.2722" font-family="Times,serif" font-size="14.00" fill="#000000">r124356</text> +</g> +<!-- tag2 --> +<g id="node2" class="node"> +<title>tag2</title> +<ellipse fill="#000000" stroke="#000000" cx="92.414" cy="-45" rx="3.5" ry="3.5"/> +</g> +<!-- index2 --> +<g id="node5" class="node"> +<title>index2</title> +<polygon fill="none" stroke="#000000" points="203.414,-63 155.414,-63 155.414,-27 209.414,-27 209.414,-57 203.414,-63"/> +<polyline fill="none" stroke="#000000" points="203.414,-63 203.414,-57 "/> +<polyline fill="none" stroke="#000000" points="209.414,-57 203.414,-57 "/> +<text text-anchor="middle" x="182.414" y="-41.3" font-family="Times,serif" font-size="14.00" fill="#000000">index</text> +</g> +<!-- tag2->index2 --> +<g id="edge2" class="edge"> +<title>tag2:head->index2</title> +<path fill="none" stroke="#000000" d="M96.2812,-45C104.7802,-45 125.8583,-45 145.0347,-45"/> +<polygon fill="#000000" stroke="#000000" points="145.1057,-48.5001 155.1057,-45 145.1056,-41.5001 145.1057,-48.5001"/> +<text text-anchor="middle" x="89.7919" y="-61.2722" font-family="Times,serif" font-size="14.00" fill="#000000">stable-release</text> +</g> +<!-- tag3 --> +<g id="node3" class="node"> +<title>tag3</title> +<ellipse fill="#000000" stroke="#000000" cx="92.414" cy="-153" rx="3.5" ry="3.5"/> +</g> +<!-- image --> +<g id="node6" class="node"> +<title>image</title> +<polygon fill="none" stroke="#000000" points="203.414,-171 155.414,-171 155.414,-135 209.414,-135 209.414,-165 203.414,-171"/> +<polyline fill="none" stroke="#000000" points="203.414,-171 203.414,-165 "/> +<polyline fill="none" stroke="#000000" points="209.414,-165 203.414,-165 "/> +<text text-anchor="middle" x="182.414" y="-149.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +</g> +<!-- tag3->image --> +<g id="edge3" class="edge"> +<title>tag3:head->image</title> +<path fill="none" stroke="#000000" d="M96.2812,-153C104.7802,-153 125.8583,-153 145.0347,-153"/> +<polygon fill="#000000" stroke="#000000" points="145.1057,-156.5001 155.1057,-153 145.1056,-149.5001 145.1057,-156.5001"/> +<text text-anchor="middle" x="89.7919" y="-169.2722" font-family="Times,serif" font-size="14.00" fill="#000000">v1.0</text> +</g> +<!-- index->index2 --> +<g id="edge6" class="edge"> +<title>index->index2</title> +<path fill="none" stroke="#000000" d="M119.417,-82.7982C127.8826,-77.7188 137.3656,-72.029 146.353,-66.6366"/> +<polygon fill="#000000" stroke="#000000" points="148.3449,-69.5232 155.1191,-61.377 144.7434,-63.5208 148.3449,-69.5232"/> +</g> +<!-- index->image --> +<g id="edge4" class="edge"> +<title>index->image</title> +<path fill="none" stroke="#000000" d="M119.417,-115.2018C127.8826,-120.2812 137.3656,-125.971 146.353,-131.3634"/> +<polygon fill="#000000" stroke="#000000" points="144.7434,-134.4792 155.1191,-136.623 148.3449,-128.4768 144.7434,-134.4792"/> +</g> +<!-- xml --> +<g id="node9" class="node"> +<title>xml</title> +<ellipse fill="none" stroke="#000000" cx="182.414" cy="-99" rx="27" ry="18"/> +<text text-anchor="middle" x="182.414" y="-95.3" font-family="Times,serif" font-size="14.00" fill="#000000">xml</text> +</g> +<!-- index->xml --> +<g id="edge5" class="edge"> +<title>index->xml</title> +<path fill="none" stroke="#000000" d="M119.417,-99C127.4417,-99 136.3806,-99 144.9449,-99"/> +<polygon fill="#000000" stroke="#000000" points="145.1191,-102.5001 155.1191,-99 145.119,-95.5001 145.1191,-102.5001"/> +</g> +<!-- image2 --> +<g id="node7" class="node"> +<title>image2</title> +<polygon fill="none" stroke="#000000" points="293.414,-90 245.414,-90 245.414,-54 299.414,-54 299.414,-84 293.414,-90"/> +<polyline fill="none" stroke="#000000" points="293.414,-90 293.414,-84 "/> +<polyline fill="none" stroke="#000000" points="299.414,-84 293.414,-84 "/> +<text text-anchor="middle" x="272.414" y="-68.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +</g> +<!-- index2->image2 --> +<g id="edge7" class="edge"> +<title>index2->image2</title> +<path fill="none" stroke="#000000" d="M209.417,-53.1009C217.6181,-55.5612 226.7739,-58.308 235.509,-60.9285"/> +<polygon fill="#000000" stroke="#000000" points="234.535,-64.2904 245.1191,-63.8115 236.5465,-57.5856 234.535,-64.2904"/> +</g> +<!-- image3 --> +<g id="node8" class="node"> +<title>image3</title> +<polygon fill="none" stroke="#000000" points="293.414,-36 245.414,-36 245.414,0 299.414,0 299.414,-30 293.414,-36"/> +<polyline fill="none" stroke="#000000" points="293.414,-36 293.414,-30 "/> +<polyline fill="none" stroke="#000000" points="299.414,-30 293.414,-30 "/> +<text text-anchor="middle" x="272.414" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +</g> +<!-- index2->image3 --> +<g id="edge8" class="edge"> +<title>index2->image3</title> +<path fill="none" stroke="#000000" d="M209.417,-36.8991C217.6181,-34.4388 226.7739,-31.692 235.509,-29.0715"/> +<polygon fill="#000000" stroke="#000000" points="236.5465,-32.4144 245.1191,-26.1885 234.535,-25.7096 236.5465,-32.4144"/> +</g> +</g> +</svg> diff --git a/images/index-anatomy.dot.svg b/images/index-anatomy.dot.svg new file mode 100644 index 0000000..55e16a6 --- /dev/null +++ b/images/index-anatomy.dot.svg @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="209pt" height="143pt" + viewBox="0.00 0.00 208.91 143.25" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 139.2506)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-139.2506 204.914,-139.2506 204.914,4 -4,4"/> +<!-- tag --> +<g id="node1" class="node"> +<title>tag</title> +<ellipse fill="#000000" stroke="#000000" cx="17.414" cy="-67.6362" rx="3.5" ry="3.5"/> +</g> +<!-- index --> +<g id="node4" class="node"> +<title>index</title> +<polygon fill="none" stroke="#000000" points="104.914,-85.6362 56.914,-85.6362 56.914,-49.6362 110.914,-49.6362 110.914,-79.6362 104.914,-85.6362"/> +<polyline fill="none" stroke="#000000" points="104.914,-85.6362 104.914,-79.6362 "/> +<polyline fill="none" stroke="#000000" points="110.914,-79.6362 104.914,-79.6362 "/> +<text text-anchor="middle" x="83.914" y="-63.9362" font-family="Times,serif" font-size="14.00" fill="#000000">index</text> +</g> +<!-- tag->index --> +<g id="edge1" class="edge"> +<title>tag:head->index</title> +<path fill="none" stroke="#000000" d="M20.9894,-67.6362C26.1004,-67.6362 36.0446,-67.6362 46.5454,-67.6362"/> +<polygon fill="#000000" stroke="#000000" points="46.6441,-71.1363 56.6441,-67.6362 46.644,-64.1363 46.6441,-71.1363"/> +<text text-anchor="middle" x="14.5" y="-83.9084" font-family="Times,serif" font-size="14.00" fill="#000000">latest</text> +</g> +<!-- tag2 --> +<g id="node2" class="node"> +<title>tag2</title> +<ellipse fill="#000000" stroke="#000000" cx="83.914" cy="-107.6362" rx="3.5" ry="3.5"/> +</g> +<!-- image --> +<g id="node5" class="node"> +<title>image</title> +<polygon fill="none" stroke="#000000" points="194.914,-118.6362 146.914,-118.6362 146.914,-82.6362 200.914,-82.6362 200.914,-112.6362 194.914,-118.6362"/> +<polyline fill="none" stroke="#000000" points="194.914,-118.6362 194.914,-112.6362 "/> +<polyline fill="none" stroke="#000000" points="200.914,-112.6362 194.914,-112.6362 "/> +<text text-anchor="middle" x="173.914" y="-96.9362" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +</g> +<!-- tag2->image --> +<g id="edge2" class="edge"> +<title>tag2:head->image</title> +<path fill="none" stroke="#000000" d="M87.7812,-107.3354C96.2802,-106.6744 117.3583,-105.035 136.5347,-103.5435"/> +<polygon fill="#000000" stroke="#000000" points="136.9072,-107.0251 146.6057,-102.7602 136.3644,-100.0462 136.9072,-107.0251"/> +<text text-anchor="middle" x="82.8601" y="-124.0506" font-family="Times,serif" font-size="14.00" fill="#000000">amd64</text> +</g> +<!-- tag3 --> +<g id="node3" class="node"> +<title>tag3</title> +<ellipse fill="#000000" stroke="#000000" cx="83.914" cy="-27.6362" rx="3.5" ry="3.5"/> +</g> +<!-- image2 --> +<g id="node6" class="node"> +<title>image2</title> +<polygon fill="none" stroke="#000000" points="194.914,-58.6362 146.914,-58.6362 146.914,-22.6362 200.914,-22.6362 200.914,-52.6362 194.914,-58.6362"/> +<polyline fill="none" stroke="#000000" points="194.914,-58.6362 194.914,-52.6362 "/> +<polyline fill="none" stroke="#000000" points="200.914,-52.6362 194.914,-52.6362 "/> +<text text-anchor="middle" x="173.914" y="-36.9362" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +</g> +<!-- tag3->image2 --> +<g id="edge3" class="edge"> +<title>tag3:head->image2</title> +<path fill="none" stroke="#000000" d="M87.7812,-28.1948C96.2802,-29.4224 117.3583,-32.467 136.5347,-35.2369"/> +<polygon fill="#000000" stroke="#000000" points="136.208,-38.726 146.6057,-36.6916 137.2088,-31.7979 136.208,-38.726"/> +<text text-anchor="middle" x="84.2138" y="-3.8" font-family="Times,serif" font-size="14.00" fill="#000000">ppc64le</text> +</g> +<!-- index->image --> +<g id="edge4" class="edge"> +<title>index->image</title> +<path fill="none" stroke="#000000" d="M110.917,-77.5373C119.1181,-80.5443 128.2739,-83.9015 137.009,-87.1043"/> +<polygon fill="#000000" stroke="#000000" points="136.0254,-90.4715 146.6191,-90.628 138.4352,-83.8993 136.0254,-90.4715"/> +</g> +<!-- index->image2 --> +<g id="edge5" class="edge"> +<title>index->image2</title> +<path fill="none" stroke="#000000" d="M110.917,-59.5353C119.1181,-57.075 128.2739,-54.3282 137.009,-51.7077"/> +<polygon fill="#000000" stroke="#000000" points="138.0465,-55.0506 146.6191,-48.8247 136.035,-48.3458 138.0465,-55.0506"/> +</g> +</g> +</svg> diff --git a/images/mutate.dot.svg b/images/mutate.dot.svg new file mode 100644 index 0000000..e493588 --- /dev/null +++ b/images/mutate.dot.svg @@ -0,0 +1,250 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="510pt" height="413pt" + viewBox="0.00 0.00 510.00 413.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 409)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-409 506,-409 506,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_source</title> +<polygon fill="none" stroke="#000000" points="28,-322 28,-397 473,-397 473,-322 28,-322"/> +<text text-anchor="middle" x="250.5" y="-381.8" font-family="Times,serif" font-size="14.00" fill="#000000">Sources</text> +</g> +<g id="clust2" class="cluster"> +<title>cluster_mutate</title> +<polygon fill="none" stroke="#000000" points="8,-175 8,-250 479,-250 479,-175 8,-175"/> +<text text-anchor="middle" x="243.5" y="-234.8" font-family="Times,serif" font-size="14.00" fill="#000000">mutate</text> +</g> +<g id="clust3" class="cluster"> +<title>cluster_sinks</title> +<polygon fill="none" stroke="#000000" points="8,-8 8,-83 494,-83 494,-8 8,-8"/> +<text text-anchor="middle" x="251" y="-15.8" font-family="Times,serif" font-size="14.00" fill="#000000">Sinks</text> +</g> +<!-- input --> +<g id="node1" class="node"> +<title>input</title> +<polygon fill="none" stroke="#000000" points="287,-294 219,-294 219,-258 287,-258 287,-294"/> +<text text-anchor="middle" x="253" y="-272.3" font-family="Times,serif" font-size="14.00" fill="#000000">v1.Image</text> +</g> +<!-- mutateconfig --> +<g id="node8" class="node"> +<title>mutateconfig</title> +<ellipse fill="none" stroke="#000000" cx="436" cy="-201" rx="35.194" ry="18"/> +<text text-anchor="middle" x="436" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Config</text> +</g> +<!-- input->mutateconfig --> +<g id="edge6" class="edge"> +<title>input->mutateconfig</title> +<path fill="none" stroke="#000000" d="M287.1531,-272.1612C322.3405,-267.7595 374.4502,-259.863 392,-250 402.3783,-244.1674 411.5932,-234.9731 418.8743,-226.1167"/> +<polygon fill="#000000" stroke="#000000" points="421.657,-228.2396 424.9952,-218.1844 416.1151,-223.9632 421.657,-228.2396"/> +</g> +<!-- mutatetime --> +<g id="node9" class="node"> +<title>mutatetime</title> +<ellipse fill="none" stroke="#000000" cx="353" cy="-201" rx="29.795" ry="18"/> +<text text-anchor="middle" x="353" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Time</text> +</g> +<!-- input->mutatetime --> +<g id="edge7" class="edge"> +<title>input->mutatetime</title> +<path fill="none" stroke="#000000" d="M287.2346,-264.8998C296.4336,-261.0133 306.0312,-256.0671 314,-250 322.4749,-243.5475 330.1749,-234.8575 336.452,-226.5875"/> +<polygon fill="#000000" stroke="#000000" points="339.4615,-228.397 342.451,-218.2327 333.7754,-224.3142 339.4615,-228.397"/> +</g> +<!-- mutatemediatype --> +<g id="node10" class="node"> +<title>mutatemediatype</title> +<ellipse fill="none" stroke="#000000" cx="253" cy="-201" rx="51.9908" ry="18"/> +<text text-anchor="middle" x="253" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">MediaType</text> +</g> +<!-- input->mutatemediatype --> +<g id="edge8" class="edge"> +<title>input->mutatemediatype</title> +<path fill="none" stroke="#000000" d="M253,-257.8446C253,-249.3401 253,-239.0076 253,-229.4964"/> +<polygon fill="#000000" stroke="#000000" points="256.5001,-229.2481 253,-219.2482 249.5001,-229.2482 256.5001,-229.2481"/> +</g> +<!-- mutateappend --> +<g id="node11" class="node"> +<title>mutateappend</title> +<ellipse fill="none" stroke="#000000" cx="145" cy="-201" rx="38.1938" ry="18"/> +<text text-anchor="middle" x="145" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Append</text> +</g> +<!-- input->mutateappend --> +<g id="edge9" class="edge"> +<title>input->mutateappend</title> +<path fill="none" stroke="#000000" d="M218.8031,-264.276C209.7024,-260.4082 200.1455,-255.6275 192,-250 182.1987,-243.2286 172.8112,-234.167 165.0383,-225.6788"/> +<polygon fill="#000000" stroke="#000000" points="167.6237,-223.3188 158.3882,-218.1268 162.3702,-227.9449 167.6237,-223.3188"/> +</g> +<!-- mutaterebase --> +<g id="node12" class="node"> +<title>mutaterebase</title> +<ellipse fill="none" stroke="#000000" cx="52" cy="-201" rx="36.2938" ry="18"/> +<text text-anchor="middle" x="52" y="-197.3" font-family="Times,serif" font-size="14.00" fill="#000000">Rebase</text> +</g> +<!-- input->mutaterebase --> +<g id="edge10" class="edge"> +<title>input->mutaterebase</title> +<path fill="none" stroke="#000000" d="M218.8523,-272.8393C179.727,-268.7745 118.3864,-260.9153 98,-250 87.2892,-244.2652 77.6813,-235.0892 70.0511,-226.22"/> +<polygon fill="#000000" stroke="#000000" points="72.6328,-223.8461 63.6249,-218.2686 67.1885,-228.2462 72.6328,-223.8461"/> +</g> +<!-- output --> +<g id="node2" class="node"> +<title>output</title> +<polygon fill="none" stroke="#000000" points="287,-147 219,-147 219,-111 287,-111 287,-147"/> +<text text-anchor="middle" x="253" y="-125.3" font-family="Times,serif" font-size="14.00" fill="#000000">v1.Image</text> +</g> +<!-- remotesink --> +<g id="node13" class="node"> +<title>remotesink</title> +<ellipse fill="none" stroke="#000000" cx="450" cy="-57" rx="35.9954" ry="18"/> +<text text-anchor="middle" x="450" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">remote</text> +</g> +<!-- output->remotesink --> +<g id="edge20" class="edge"> +<title>output->remotesink</title> +<path fill="none" stroke="#000000" d="M287.303,-121.0277C318.6434,-113.1846 365.9147,-99.9363 405,-83 409.5224,-81.0404 414.1637,-78.7332 418.6672,-76.3161"/> +<polygon fill="#000000" stroke="#000000" points="420.6325,-79.2265 427.6357,-71.2763 417.2032,-73.1241 420.6325,-79.2265"/> +</g> +<!-- tarballsink --> +<g id="node14" class="node"> +<title>tarballsink</title> +<ellipse fill="none" stroke="#000000" cx="363" cy="-57" rx="33.2948" ry="18"/> +<text text-anchor="middle" x="363" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">tarball</text> +</g> +<!-- output->tarballsink --> +<g id="edge19" class="edge"> +<title>output->tarballsink</title> +<path fill="none" stroke="#000000" d="M280.7576,-110.8314C296.6532,-100.427 316.6101,-87.3643 332.8841,-76.7122"/> +<polygon fill="#000000" stroke="#000000" points="335.0468,-79.4798 341.497,-71.0747 331.2132,-73.6228 335.0468,-79.4798"/> +</g> +<!-- legacy/tarballsink --> +<g id="node15" class="node"> +<title>legacy/tarballsink</title> +<ellipse fill="none" stroke="#000000" cx="253" cy="-57" rx="58.4896" ry="18"/> +<text text-anchor="middle" x="253" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">legacy/tarball</text> +</g> +<!-- output->legacy/tarballsink --> +<g id="edge16" class="edge"> +<title>output->legacy/tarballsink</title> +<path fill="none" stroke="#000000" d="M253,-110.8314C253,-103.131 253,-93.9743 253,-85.4166"/> +<polygon fill="#000000" stroke="#000000" points="256.5001,-85.4132 253,-75.4133 249.5001,-85.4133 256.5001,-85.4132"/> +</g> +<!-- layoutsink --> +<g id="node16" class="node"> +<title>layoutsink</title> +<ellipse fill="none" stroke="#000000" cx="144" cy="-57" rx="32.4942" ry="18"/> +<text text-anchor="middle" x="144" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">layout</text> +</g> +<!-- output->layoutsink --> +<g id="edge17" class="edge"> +<title>output->layoutsink</title> +<path fill="none" stroke="#000000" d="M225.4947,-110.8314C209.6373,-100.3567 189.7007,-87.1876 173.5158,-76.4967"/> +<polygon fill="#000000" stroke="#000000" points="175.2329,-73.4363 164.9598,-70.845 171.3748,-79.2771 175.2329,-73.4363"/> +</g> +<!-- daemonsink --> +<g id="node17" class="node"> +<title>daemonsink</title> +<ellipse fill="none" stroke="#000000" cx="55" cy="-57" rx="38.9931" ry="18"/> +<text text-anchor="middle" x="55" y="-53.3" font-family="Times,serif" font-size="14.00" fill="#000000">daemon</text> +</g> +<!-- output->daemonsink --> +<g id="edge18" class="edge"> +<title>output->daemonsink</title> +<path fill="none" stroke="#000000" d="M218.9614,-120.6358C188.1089,-112.5708 141.6852,-99.2189 103,-83 98.2065,-80.9903 93.2639,-78.6484 88.456,-76.2089"/> +<polygon fill="#000000" stroke="#000000" points="89.9289,-73.0288 79.451,-71.4589 86.663,-79.2202 89.9289,-73.0288"/> +</g> +<!-- remotesource --> +<g id="node3" class="node"> +<title>remotesource</title> +<ellipse fill="none" stroke="#000000" cx="429" cy="-348" rx="35.9954" ry="18"/> +<text text-anchor="middle" x="429" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">remote</text> +</g> +<!-- remotesource->input --> +<g id="edge5" class="edge"> +<title>remotesource->input</title> +<path fill="none" stroke="#000000" d="M406.5444,-333.9282C399.4756,-329.7994 391.5425,-325.4665 384,-322 355.7704,-309.0257 322.7595,-297.4554 296.9378,-289.1642"/> +<polygon fill="#000000" stroke="#000000" points="297.6528,-285.7195 287.0628,-286.0401 295.5414,-292.3935 297.6528,-285.7195"/> +</g> +<!-- tarballsource --> +<g id="node4" class="node"> +<title>tarballsource</title> +<ellipse fill="none" stroke="#000000" cx="342" cy="-348" rx="33.2948" ry="18"/> +<text text-anchor="middle" x="342" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">tarball</text> +</g> +<!-- tarballsource->input --> +<g id="edge4" class="edge"> +<title>tarballsource->input</title> +<path fill="none" stroke="#000000" d="M323.1254,-332.7307C311.5473,-323.3641 296.4585,-311.1575 283.2447,-300.4676"/> +<polygon fill="#000000" stroke="#000000" points="285.2835,-297.6151 275.3077,-294.0467 280.8809,-303.0573 285.2835,-297.6151"/> +</g> +<!-- randomsource --> +<g id="node5" class="node"> +<title>randomsource</title> +<ellipse fill="none" stroke="#000000" cx="253" cy="-348" rx="38.1938" ry="18"/> +<text text-anchor="middle" x="253" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">random</text> +</g> +<!-- randomsource->input --> +<g id="edge1" class="edge"> +<title>randomsource->input</title> +<path fill="none" stroke="#000000" d="M253,-329.8314C253,-322.131 253,-312.9743 253,-304.4166"/> +<polygon fill="#000000" stroke="#000000" points="256.5001,-304.4132 253,-294.4133 249.5001,-304.4133 256.5001,-304.4132"/> +</g> +<!-- layoutsource --> +<g id="node6" class="node"> +<title>layoutsource</title> +<ellipse fill="none" stroke="#000000" cx="164" cy="-348" rx="32.4942" ry="18"/> +<text text-anchor="middle" x="164" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">layout</text> +</g> +<!-- layoutsource->input --> +<g id="edge2" class="edge"> +<title>layoutsource->input</title> +<path fill="none" stroke="#000000" d="M182.4409,-333.0816C193.9939,-323.7353 209.165,-311.462 222.4783,-300.6917"/> +<polygon fill="#000000" stroke="#000000" points="224.9055,-303.2301 230.4786,-294.2195 220.5028,-297.788 224.9055,-303.2301"/> +</g> +<!-- daemonsource --> +<g id="node7" class="node"> +<title>daemonsource</title> +<ellipse fill="none" stroke="#000000" cx="75" cy="-348" rx="38.9931" ry="18"/> +<text text-anchor="middle" x="75" y="-344.3" font-family="Times,serif" font-size="14.00" fill="#000000">daemon</text> +</g> +<!-- daemonsource->input --> +<g id="edge3" class="edge"> +<title>daemonsource->input</title> +<path fill="none" stroke="#000000" d="M99.5417,-333.7511C106.9591,-329.7065 115.2057,-325.4645 123,-322 151.0042,-309.5525 183.5065,-298.0518 209.0065,-289.6634"/> +<polygon fill="#000000" stroke="#000000" points="210.3357,-292.9117 218.7644,-286.4925 208.1722,-286.2544 210.3357,-292.9117"/> +</g> +<!-- mutateconfig->output --> +<g id="edge11" class="edge"> +<title>mutateconfig->output</title> +<path fill="none" stroke="#000000" d="M414.1129,-186.8193C407.2039,-182.686 399.4313,-178.3781 392,-175 361.1889,-160.9941 324.8265,-149.1138 297.0223,-140.9367"/> +<polygon fill="#000000" stroke="#000000" points="297.7707,-137.5098 287.192,-138.0946 295.8264,-144.2344 297.7707,-137.5098"/> +</g> +<!-- mutatetime->output --> +<g id="edge12" class="edge"> +<title>mutatetime->output</title> +<path fill="none" stroke="#000000" d="M333.719,-187.1177C320.4063,-177.5325 302.3499,-164.5319 286.6904,-153.2571"/> +<polygon fill="#000000" stroke="#000000" points="288.4431,-150.2062 278.2827,-147.2035 284.353,-155.887 288.4431,-150.2062"/> +</g> +<!-- mutatemediatype->output --> +<g id="edge13" class="edge"> +<title>mutatemediatype->output</title> +<path fill="none" stroke="#000000" d="M253,-182.8314C253,-175.131 253,-165.9743 253,-157.4166"/> +<polygon fill="#000000" stroke="#000000" points="256.5001,-157.4132 253,-147.4133 249.5001,-157.4133 256.5001,-157.4132"/> +</g> +<!-- mutateappend->output --> +<g id="edge14" class="edge"> +<title>mutateappend->output</title> +<path fill="none" stroke="#000000" d="M167.116,-186.256C181.6204,-176.5864 200.9061,-163.7292 217.5271,-152.6486"/> +<polygon fill="#000000" stroke="#000000" points="219.5574,-155.5016 225.9365,-147.0423 215.6745,-149.6772 219.5574,-155.5016"/> +</g> +<!-- mutaterebase->output --> +<g id="edge15" class="edge"> +<title>mutaterebase->output</title> +<path fill="none" stroke="#000000" d="M74.8826,-186.7207C82.1056,-182.5835 90.2314,-178.298 98,-175 134.3708,-159.5595 177.6081,-147.1912 209.1638,-139.185"/> +<polygon fill="#000000" stroke="#000000" points="210.118,-142.5543 218.9752,-136.7405 208.4256,-135.7619 210.118,-142.5543"/> +</g> +</g> +</svg> diff --git a/images/ociimage.gv b/images/ociimage.gv new file mode 100644 index 0000000..5fbe947 --- /dev/null +++ b/images/ociimage.gv @@ -0,0 +1,97 @@ +digraph ociimage { + rankdir=LR; + node [shape=box]; + edge [splines=polyline]; + lrank [style=invisible][color=white]; + + "manifest A"[label=<<table border="0"> + <tr><td align="center">image manifest (platform A)</td></tr> + <tr><td align="center"></td></tr> + <tr><td align="left">- schema version</td></tr> + <tr><td align="left">- media type</td></tr> + <tr><td align="left">- config : descriptor</td></tr> + <tr><td align="left">- layers : array of descriptors</td></tr> + <tr><td align="left">- (annotations)</td></tr> + </table>>]; + + "image index"[label=<<table border="0"> + <tr><td align="center">image index</td></tr> + <tr><td></td></tr> + <tr><td align="left">- schema version</td></tr> + <tr><td align="left">- media type</td></tr> + <tr><td align="left">- manifests : array of descriptors</td></tr> + <tr><td align="left">- (annotations)</td></tr> + </table>>]; + + // references + edge [color=red][style=dashed]; + client [style=invisible][color=white]; + client -> "image index"[label="image reference"]; + client -> "manifest A"[label="image reference"]; + + // descriptors + edge [color=brown][style=solid]; + "image index" -> "manifest A"; + "image index" -> "image manifest (platform B)"; + "configuration"[label=<<table border="0"> + <tr><td align="center">configuration</td></tr> + <tr><td></td></tr> + <tr><td align="left">- rootfs/diff_ids : array of layer ids</td></tr> + <tr><td align="left">- container config</td></tr> + <tr><td align="left">- history</td></tr> + </table>>]; + "manifest A" -> "configuration"; + "layer 0"[label=<<table border="0"> + <tr><td align="center">layer</td></tr> + <tr><td></td></tr> + <tr><td align="left">file system additions, overwrites, and deletions</td></tr> + </table>>]; + "layer 1"[label=layer]; + "layer 2"[label=layer]; + "manifest A" -> "layer 0"[label=0]; + "manifest A" -> "layer 1"[label=1]; + "manifest A" -> "layer 2"[label=2]; + + // ids + edge [color=blue][style=dotted]; + "client" -> "configuration"[label="image id"]; + "configuration" -> "layer 0"[label=0]; + "configuration" -> "layer 1"[label=1]; + "configuration" -> "layer 2"[label=2]; + + // key + subgraph cluster { + k1 [label="Key:"][peripheries="0"]; + node [style=invisible][color=white]; + k2; + k3; + k4; + node [style=solid][color=black]; + k1 -> k2[color=red][style=dashed][label=<<table border="0"> + <tr><td align="center">image reference</td></tr> + <tr><td></td></tr> + <tr><td align="left">- hostname</td></tr> + <tr><td align="left">- path</td></tr> + <tr><td align="left">- (tag)</td></tr> + <tr><td align="left">- (SHA-256 digest of compressed content)</td></tr> + </table>>]; + k2 -> k3[color=brown][style=solid][label=<<table border="0"> + <tr><td align="center">descriptor</td></tr> + <tr><td></td></tr> + <tr><td align="left">targets content with the following properties:</td></tr> + <tr><td align="left">- media type</td></tr> + <tr><td align="left">- SHA-256 digest of compressed content</td></tr> + <tr><td align="left">- size</td></tr> + <tr><td align="left">- (urls)</td></tr> + <tr><td align="left">- (annotations)</td></tr> + </table>>]; + k3 -> k4[color=blue][style=dotted][label=<<table border="0"> + <tr><td align="center">id</td></tr> + <tr><td></td></tr> + <tr><td align="left">- SHA-256 digest of uncompressed content</td></tr> + </table>>]; + } + + { rank=same; lrank -> "layer 2" -> "layer 1" -> "layer 0" [style=invis] } + { rank=same; "manifest A", "image manifest (platform B)" } +}
\ No newline at end of file diff --git a/images/ociimage.jpeg b/images/ociimage.jpeg Binary files differnew file mode 100644 index 0000000..b1e0ca5 --- /dev/null +++ b/images/ociimage.jpeg diff --git a/images/remote.dot.svg b/images/remote.dot.svg new file mode 100644 index 0000000..a4b5fae --- /dev/null +++ b/images/remote.dot.svg @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="490pt" height="368pt" + viewBox="0.00 0.00 489.55 368.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 364)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-364 485.5499,-364 485.5499,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_registry</title> +<polygon fill="none" stroke="#000000" points="0,-8 0,-352 481.5499,-352 481.5499,-8 0,-8"/> +<text text-anchor="middle" x="240.7749" y="-336.8" font-family="Times,serif" font-size="14.00" fill="#000000">registry</text> +</g> +<g id="clust2" class="cluster"> +<title>cluster_tags</title> +<polygon fill="none" stroke="#000000" points="8,-24 8,-153 103,-153 103,-24 8,-24"/> +<text text-anchor="middle" x="55.5" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../tags/list</text> +</g> +<g id="clust3" class="cluster"> +<title>cluster_manifests</title> +<polygon fill="none" stroke="#000000" points="123,-16 123,-321 314,-321 314,-16 123,-16"/> +<text text-anchor="middle" x="218.5" y="-305.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../manifests/<ref></text> +</g> +<g id="clust4" class="cluster"> +<title>cluster_manifest</title> +<polygon fill="none" stroke="#000000" points="236,-24 236,-153 306,-153 306,-24 236,-24"/> +<text text-anchor="middle" x="271" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text> +</g> +<g id="clust5" class="cluster"> +<title>cluster_manifest2</title> +<polygon fill="none" stroke="#000000" points="236,-161 236,-290 306,-290 306,-161 236,-161"/> +<text text-anchor="middle" x="271" y="-274.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text> +</g> +<g id="clust6" class="cluster"> +<title>cluster_index</title> +<polygon fill="none" stroke="#000000" points="131,-78 131,-153 216,-153 216,-78 131,-78"/> +<text text-anchor="middle" x="173.5" y="-137.8" font-family="Times,serif" font-size="14.00" fill="#000000">index</text> +</g> +<g id="clust7" class="cluster"> +<title>cluster_blobs</title> +<polygon fill="none" stroke="#000000" points="334,-20 334,-311 473.5499,-311 473.5499,-20 334,-20"/> +<text text-anchor="middle" x="403.7749" y="-295.8" font-family="Times,serif" font-size="14.00" fill="#000000">/v2/.../blobs/<sha256></text> +</g> +<!-- tag --> +<g id="node1" class="node"> +<title>tag</title> +<polygon fill="none" stroke="#000000" points="82,-68 28,-68 28,-32 82,-32 82,-68"/> +<text text-anchor="middle" x="55" y="-46.3" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text> +</g> +<!-- mconfig --> +<g id="node3" class="node"> +<title>mconfig</title> +<polygon fill="none" stroke="#000000" points="298,-68 244,-68 244,-32 298,-32 298,-68"/> +<text text-anchor="middle" x="271" y="-46.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- tag->mconfig --> +<g id="edge9" class="edge"> +<title>tag->mconfig</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M82.3565,-50C120.7403,-50 190.6983,-50 233.7878,-50"/> +<polygon fill="#000000" stroke="#000000" points="226.0002,-53.5005 236,-50 225.9998,-46.5005 226.0002,-53.5005"/> +</g> +<!-- tag2 --> +<g id="node2" class="node"> +<title>tag2</title> +<polygon fill="none" stroke="#000000" points="82,-122 28,-122 28,-86 82,-86 82,-122"/> +<text text-anchor="middle" x="55" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">tag</text> +</g> +<!-- imanifest --> +<g id="node7" class="node"> +<title>imanifest</title> +<polygon fill="none" stroke="#000000" points="208,-122 139,-122 139,-86 208,-86 208,-122"/> +<text text-anchor="middle" x="173.5" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifests</text> +</g> +<!-- tag2->imanifest --> +<g id="edge10" class="edge"> +<title>tag2->imanifest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M82.1863,-104C95.9752,-104 113.0796,-104 128.744,-104"/> +<polygon fill="#000000" stroke="#000000" points="121.0002,-107.5004 131,-104 120.9998,-100.5004 121.0002,-107.5004"/> +</g> +<!-- bconfig --> +<g id="node8" class="node"> +<title>bconfig</title> +<polygon fill="none" stroke="#000000" points="441.3261,-46 422.3005,-64 384.2493,-64 365.2238,-46 384.2493,-28 422.3005,-28 441.3261,-46"/> +<text text-anchor="middle" x="403.2749" y="-42.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- mconfig->bconfig --> +<g id="edge7" class="edge"> +<title>mconfig->bconfig</title> +<path fill="none" stroke="#000000" d="M298.087,-49.1809C314.7449,-48.6772 336.552,-48.0177 355.9889,-47.4299"/> +<polygon fill="#000000" stroke="#000000" points="356.3057,-50.922 366.1953,-47.1213 356.0941,-43.9252 356.3057,-50.922"/> +</g> +<!-- layers --> +<g id="node4" class="node"> +<title>layers</title> +<polygon fill="none" stroke="#000000" points="298,-122 244,-122 244,-86 298,-86 298,-122"/> +<text text-anchor="middle" x="271" y="-100.3" font-family="Times,serif" font-size="14.00" fill="#000000">layers</text> +</g> +<!-- l1 --> +<g id="node10" class="node"> +<title>l1</title> +<polygon fill="none" stroke="#000000" points="430.2749,-118 427.2749,-122 406.2749,-122 403.2749,-118 376.2749,-118 376.2749,-82 430.2749,-82 430.2749,-118"/> +<text text-anchor="middle" x="403.2749" y="-96.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text> +</g> +<!-- layers->l1 --> +<g id="edge3" class="edge"> +<title>layers->l1</title> +<path fill="none" stroke="#000000" d="M298.087,-103.1809C317.7033,-102.5877 344.4602,-101.7786 366.0782,-101.1248"/> +<polygon fill="#000000" stroke="#000000" points="366.3345,-104.6188 376.2242,-100.818 366.1229,-97.622 366.3345,-104.6188"/> +</g> +<!-- l2 --> +<g id="node11" class="node"> +<title>l2</title> +<polygon fill="none" stroke="#000000" points="430.2749,-172 427.2749,-176 406.2749,-176 403.2749,-172 376.2749,-172 376.2749,-136 430.2749,-136 430.2749,-172"/> +<text text-anchor="middle" x="403.2749" y="-150.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text> +</g> +<!-- layers->l2 --> +<g id="edge4" class="edge"> +<title>layers->l2</title> +<path fill="none" stroke="#000000" d="M298.087,-114.2389C317.8816,-121.7213 344.9472,-131.9521 366.6665,-140.162"/> +<polygon fill="#000000" stroke="#000000" points="365.6326,-143.5128 376.2242,-143.7748 368.1077,-136.965 365.6326,-143.5128"/> +</g> +<!-- mconfig2 --> +<g id="node5" class="node"> +<title>mconfig2</title> +<polygon fill="none" stroke="#000000" points="298,-259 244,-259 244,-223 298,-223 298,-259"/> +<text text-anchor="middle" x="271" y="-237.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- bconfig2 --> +<g id="node9" class="node"> +<title>bconfig2</title> +<polygon fill="none" stroke="#000000" points="441.3261,-262 422.3005,-280 384.2493,-280 365.2238,-262 384.2493,-244 422.3005,-244 441.3261,-262"/> +<text text-anchor="middle" x="403.2749" y="-258.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- mconfig2->bconfig2 --> +<g id="edge8" class="edge"> +<title>mconfig2->bconfig2</title> +<path fill="none" stroke="#000000" d="M298.087,-245.3003C316.0623,-248.1541 340.0335,-251.9598 360.5522,-255.2173"/> +<polygon fill="#000000" stroke="#000000" points="360.1858,-258.7029 370.6109,-256.8143 361.2834,-251.7895 360.1858,-258.7029"/> +</g> +<!-- layers2 --> +<g id="node6" class="node"> +<title>layers2</title> +<polygon fill="none" stroke="#000000" points="298,-205 244,-205 244,-169 298,-169 298,-205"/> +<text text-anchor="middle" x="271" y="-183.3" font-family="Times,serif" font-size="14.00" fill="#000000">layers</text> +</g> +<!-- layers2->l2 --> +<g id="edge5" class="edge"> +<title>layers2->l2</title> +<path fill="none" stroke="#000000" d="M298.087,-180.2423C317.7924,-175.3262 344.7036,-168.6124 366.3727,-163.2064"/> +<polygon fill="#000000" stroke="#000000" points="367.3688,-166.5652 376.2242,-160.7486 365.6743,-159.7734 367.3688,-166.5652"/> +</g> +<!-- l3 --> +<g id="node12" class="node"> +<title>l3</title> +<polygon fill="none" stroke="#000000" points="430.2749,-226 427.2749,-230 406.2749,-230 403.2749,-226 376.2749,-226 376.2749,-190 430.2749,-190 430.2749,-226"/> +<text text-anchor="middle" x="403.2749" y="-204.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text> +</g> +<!-- layers2->l3 --> +<g id="edge6" class="edge"> +<title>layers2->l3</title> +<path fill="none" stroke="#000000" d="M298.087,-191.3003C317.7033,-194.4146 344.4602,-198.6626 366.0782,-202.0946"/> +<polygon fill="#000000" stroke="#000000" points="365.799,-205.5941 376.2242,-203.7054 366.8967,-198.6807 365.799,-205.5941"/> +</g> +<!-- imanifest->mconfig --> +<g id="edge1" class="edge"> +<title>imanifest->mconfig</title> +<path fill="none" stroke="#000000" d="M206.2373,-85.8686C215.4605,-80.7603 225.5439,-75.1757 234.9494,-69.9665"/> +<polygon fill="#000000" stroke="#000000" points="230.4352,-78.131 236,-69.1149 226.0273,-72.6929 230.4352,-78.131"/> +</g> +<!-- imanifest->mconfig2 --> +<g id="edge2" class="edge"> +<title>imanifest->mconfig2</title> +<path fill="none" stroke="#000000" d="M181.0913,-122.1444C190.3822,-143.0951 207.6041,-178.2366 229.4113,-206.011"/> +<polygon fill="#000000" stroke="#000000" points="226.9372,-208.5121 236,-214 232.3376,-204.0583 226.9372,-208.5121"/> +</g> +</g> +</svg> diff --git a/images/stream.dot.svg b/images/stream.dot.svg new file mode 100644 index 0000000..3f3f04e --- /dev/null +++ b/images/stream.dot.svg @@ -0,0 +1,217 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: G Pages: 1 --> +<svg width="533pt" height="767pt" + viewBox="0.00 0.00 533.09 767.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 763)"> +<title>G</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-763 529.0946,-763 529.0946,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_goroutine</title> +<polygon fill="none" stroke="#000000" points="8,-208 8,-715 396,-715 396,-208 8,-208"/> +<text text-anchor="middle" x="202" y="-699.8" font-family="Times,serif" font-size="14.00" fill="#000000">goroutine</text> +</g> +<!-- fs --> +<g id="node1" class="node"> +<title>fs</title> +<polygon fill="none" stroke="#000000" points="166,-759 163,-763 142,-763 139,-759 112,-759 112,-723 166,-723 166,-759"/> +<text text-anchor="middle" x="139" y="-737.3" font-family="Times,serif" font-size="14.00" fill="#000000">input</text> +</g> +<!-- rc --> +<g id="node6" class="node"> +<title>rc</title> +<ellipse fill="none" stroke="#000000" cx="139" cy="-666" rx="61.1893" ry="18"/> +<text text-anchor="middle" x="139" y="-662.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.ReadCloser</text> +</g> +<!-- fs->rc --> +<g id="edge12" class="edge"> +<title>fs->rc</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M139,-722.8446C139,-714.3401 139,-704.0076 139,-694.4964"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-694.2481 139,-684.2482 135.5001,-694.2482 142.5001,-694.2481"/> +</g> +<!-- pr --> +<g id="node2" class="node"> +<title>pr</title> +<ellipse fill="none" stroke="#000000" cx="464" cy="-234" rx="60.3893" ry="18"/> +<text text-anchor="middle" x="464" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.PipeReader</text> +</g> +<!-- compressed --> +<g id="node3" class="node"> +<title>compressed</title> +<polygon fill="none" stroke="#000000" points="510.5,-180 417.5,-180 417.5,-144 510.5,-144 510.5,-180"/> +<text text-anchor="middle" x="464" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">Compressed()</text> +</g> +<!-- pr->compressed --> +<g id="edge14" class="edge"> +<title>pr->compressed</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M464,-215.8314C464,-208.131 464,-198.9743 464,-190.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-190.4132 464,-180.4133 460.5001,-190.4133 467.5001,-190.4132"/> +</g> +<!-- rc2 --> +<g id="node4" class="node"> +<title>rc2</title> +<ellipse fill="none" stroke="#000000" cx="464" cy="-90" rx="61.1893" ry="18"/> +<text text-anchor="middle" x="464" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.ReadCloser</text> +</g> +<!-- compressed->rc2 --> +<g id="edge15" class="edge"> +<title>compressed->rc2</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M464,-143.8314C464,-136.131 464,-126.9743 464,-118.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-118.4132 464,-108.4133 460.5001,-118.4133 467.5001,-118.4132"/> +</g> +<!-- output --> +<g id="node5" class="node"> +<title>output</title> +<path fill="none" stroke="#000000" d="M491,-32.7273C491,-34.5331 478.8982,-36 464,-36 449.1018,-36 437,-34.5331 437,-32.7273 437,-32.7273 437,-3.2727 437,-3.2727 437,-1.4669 449.1018,0 464,0 478.8982,0 491,-1.4669 491,-3.2727 491,-3.2727 491,-32.7273 491,-32.7273"/> +<path fill="none" stroke="#000000" d="M491,-32.7273C491,-30.9214 478.8982,-29.4545 464,-29.4545 449.1018,-29.4545 437,-30.9214 437,-32.7273"/> +<text text-anchor="middle" x="464" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">output</text> +</g> +<!-- rc2->output --> +<g id="edge16" class="edge"> +<title>rc2->output</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M464,-71.8314C464,-64.131 464,-54.9743 464,-46.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="467.5001,-46.4132 464,-36.4133 460.5001,-46.4133 467.5001,-46.4132"/> +</g> +<!-- copy --> +<g id="node7" class="node"> +<title>copy</title> +<ellipse fill="none" stroke="#000000" cx="139" cy="-594" rx="38.9931" ry="18"/> +<text text-anchor="middle" x="139" y="-590.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.Copy</text> +</g> +<!-- rc->copy --> +<g id="edge1" class="edge"> +<title>rc->copy</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M139,-647.8314C139,-640.131 139,-630.9743 139,-622.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-622.4132 139,-612.4133 135.5001,-622.4133 142.5001,-622.4132"/> +</g> +<!-- mw --> +<g id="node9" class="node"> +<title>mw</title> +<ellipse fill="none" stroke="#000000" cx="139" cy="-522" rx="63.8893" ry="18"/> +<text text-anchor="middle" x="139" y="-518.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.MultiWriter</text> +</g> +<!-- copy->mw --> +<g id="edge2" class="edge"> +<title>copy->mw</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M139,-575.8314C139,-568.131 139,-558.9743 139,-550.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="142.5001,-550.4132 139,-540.4133 135.5001,-550.4133 142.5001,-550.4132"/> +</g> +<!-- pw --> +<g id="node8" class="node"> +<title>pw</title> +<ellipse fill="none" stroke="#000000" cx="329" cy="-306" rx="59.2899" ry="18"/> +<text text-anchor="middle" x="329" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.PipeWriter</text> +</g> +<!-- pw->pr --> +<g id="edge13" class="edge"> +<title>pw->pr</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M358.6263,-290.1993C378.2335,-279.7421 404.1444,-265.923 425.3655,-254.6051"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="427.2663,-257.558 434.4428,-249.7638 423.9722,-251.3815 427.2663,-257.558"/> +</g> +<!-- h1 --> +<g id="node10" class="node"> +<title>h1</title> +<ellipse fill="none" stroke="#000000" cx="73" cy="-450" rx="54.6905" ry="18"/> +<text text-anchor="middle" x="73" y="-446.3" font-family="Times,serif" font-size="14.00" fill="#000000">sha256.New</text> +</g> +<!-- mw->h1 --> +<g id="edge3" class="edge"> +<title>mw->h1</title> +<path fill="none" stroke="#000000" d="M123.0232,-504.5708C114.8353,-495.6385 104.7218,-484.6056 95.736,-474.8029"/> +<polygon fill="#000000" stroke="#000000" points="98.167,-472.2752 88.8296,-467.2687 93.0069,-477.0053 98.167,-472.2752"/> +</g> +<!-- gzip --> +<g id="node11" class="node"> +<title>gzip</title> +<ellipse fill="none" stroke="#000000" cx="198" cy="-450" rx="51.9908" ry="18"/> +<text text-anchor="middle" x="198" y="-446.3" font-family="Times,serif" font-size="14.00" fill="#000000">gzip.Writer</text> +</g> +<!-- mw->gzip --> +<g id="edge5" class="edge"> +<title>mw->gzip</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M153.5843,-504.2022C160.7104,-495.506 169.4123,-484.8867 177.2191,-475.3598"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="180.156,-477.2978 183.7871,-467.3446 174.7416,-472.861 180.156,-477.2978"/> +</g> +<!-- diffid --> +<g id="node16" class="node"> +<title>diffid</title> +<polygon fill="none" stroke="#000000" points="104,-396 42,-396 42,-360 104,-360 104,-396"/> +<text text-anchor="middle" x="73" y="-374.3" font-family="Times,serif" font-size="14.00" fill="#000000">DiffID()</text> +</g> +<!-- h1->diffid --> +<g id="edge4" class="edge"> +<title>h1->diffid</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M73,-431.8314C73,-424.131 73,-414.9743 73,-406.4166"/> +<polygon fill="#000000" stroke="#000000" points="76.5001,-406.4132 73,-396.4133 69.5001,-406.4133 76.5001,-406.4132"/> +</g> +<!-- mw2 --> +<g id="node12" class="node"> +<title>mw2</title> +<ellipse fill="none" stroke="#000000" cx="198" cy="-378" rx="63.8893" ry="18"/> +<text text-anchor="middle" x="198" y="-374.3" font-family="Times,serif" font-size="14.00" fill="#000000">io.MultiWriter</text> +</g> +<!-- gzip->mw2 --> +<g id="edge6" class="edge"> +<title>gzip->mw2</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M198,-431.8314C198,-424.131 198,-414.9743 198,-406.4166"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="201.5001,-406.4132 198,-396.4133 194.5001,-406.4133 201.5001,-406.4132"/> +</g> +<!-- mw2->pw --> +<g id="edge11" class="edge"> +<title>mw2->pw</title> +<path fill="none" stroke="#000000" stroke-width="2" d="M227.399,-361.8418C246.2391,-351.4869 270.8742,-337.947 291.1679,-326.7932"/> +<polygon fill="#000000" stroke="#000000" stroke-width="2" points="293.0724,-329.7403 300.1502,-321.8564 289.7008,-323.6058 293.0724,-329.7403"/> +</g> +<!-- h2 --> +<g id="node13" class="node"> +<title>h2</title> +<ellipse fill="none" stroke="#000000" cx="71" cy="-306" rx="54.6905" ry="18"/> +<text text-anchor="middle" x="71" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">sha256.New</text> +</g> +<!-- mw2->h2 --> +<g id="edge7" class="edge"> +<title>mw2->h2</title> +<path fill="none" stroke="#000000" d="M169.4987,-361.8418C151.107,-351.415 127.019,-337.7588 107.2676,-326.5612"/> +<polygon fill="#000000" stroke="#000000" points="108.9593,-323.4969 98.5339,-321.6098 105.507,-329.5864 108.9593,-323.4969"/> +</g> +<!-- count --> +<g id="node14" class="node"> +<title>count</title> +<ellipse fill="none" stroke="#000000" cx="198" cy="-306" rx="53.8905" ry="18"/> +<text text-anchor="middle" x="198" y="-302.3" font-family="Times,serif" font-size="14.00" fill="#000000">countWriter</text> +</g> +<!-- mw2->count --> +<g id="edge9" class="edge"> +<title>mw2->count</title> +<path fill="none" stroke="#000000" d="M198,-359.8314C198,-352.131 198,-342.9743 198,-334.4166"/> +<polygon fill="#000000" stroke="#000000" points="201.5001,-334.4132 198,-324.4133 194.5001,-334.4133 201.5001,-334.4132"/> +</g> +<!-- digest --> +<g id="node17" class="node"> +<title>digest</title> +<polygon fill="none" stroke="#000000" points="101.5,-252 40.5,-252 40.5,-216 101.5,-216 101.5,-252"/> +<text text-anchor="middle" x="71" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">Digest()</text> +</g> +<!-- h2->digest --> +<g id="edge8" class="edge"> +<title>h2->digest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M71,-287.8314C71,-280.131 71,-270.9743 71,-262.4166"/> +<polygon fill="#000000" stroke="#000000" points="74.5001,-262.4132 71,-252.4133 67.5001,-262.4133 74.5001,-262.4132"/> +</g> +<!-- size --> +<g id="node15" class="node"> +<title>size</title> +<polygon fill="none" stroke="#000000" points="225,-252 171,-252 171,-216 225,-216 225,-252"/> +<text text-anchor="middle" x="198" y="-230.3" font-family="Times,serif" font-size="14.00" fill="#000000">Size()</text> +</g> +<!-- count->size --> +<g id="edge10" class="edge"> +<title>count->size</title> +<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M198,-287.8314C198,-280.131 198,-270.9743 198,-262.4166"/> +<polygon fill="#000000" stroke="#000000" points="201.5001,-262.4132 198,-252.4133 194.5001,-262.4133 201.5001,-262.4132"/> +</g> +</g> +</svg> diff --git a/images/tarball.dot.svg b/images/tarball.dot.svg new file mode 100644 index 0000000..4c6edc0 --- /dev/null +++ b/images/tarball.dot.svg @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: %3 Pages: 1 --> +<svg width="523pt" height="303pt" + viewBox="0.00 0.00 523.00 303.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 299)"> +<title>%3</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-299 519,-299 519,4 -4,4"/> +<g id="clust1" class="cluster"> +<title>cluster_tarball</title> +<polygon fill="none" stroke="#000000" points="8,-8 8,-287 507,-287 507,-8 8,-8"/> +<text text-anchor="middle" x="257.5" y="-271.8" font-family="Times,serif" font-size="14.00" fill="#000000">image.tar</text> +</g> +<g id="clust2" class="cluster"> +<title>cluster_manifest</title> +<polygon fill="none" stroke="#000000" points="16,-16 16,-253 123,-253 123,-16 16,-16"/> +<text text-anchor="middle" x="69.5" y="-237.8" font-family="Times,serif" font-size="14.00" fill="#000000">manifest.json</text> +</g> +<g id="clust3" class="cluster"> +<title>cluster_layer1</title> +<polygon fill="none" stroke="#000000" points="397,-34 397,-133 499,-133 499,-34 397,-34"/> +<text text-anchor="middle" x="448" y="-117.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text> +</g> +<g id="clust4" class="cluster"> +<title>cluster_layer2</title> +<polygon fill="none" stroke="#000000" points="397,-141 397,-240 499,-240 499,-141 397,-141"/> +<text text-anchor="middle" x="448" y="-224.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar.gz</text> +</g> +<!-- mconfig --> +<g id="node1" class="node"> +<title>mconfig</title> +<polygon fill="none" stroke="#000000" points="96.5,-168 42.5,-168 42.5,-132 96.5,-132 96.5,-168"/> +<text text-anchor="middle" x="69.5" y="-146.3" font-family="Times,serif" font-size="14.00" fill="#000000">Config</text> +</g> +<!-- config --> +<g id="node5" class="node"> +<title>config</title> +<polygon fill="none" stroke="#000000" points="291,-154 243,-154 243,-118 297,-118 297,-148 291,-154"/> +<polyline fill="none" stroke="#000000" points="291,-154 291,-148 "/> +<polyline fill="none" stroke="#000000" points="297,-148 291,-148 "/> +<text text-anchor="middle" x="270" y="-132.3" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</g> +<!-- mconfig->config --> +<g id="edge1" class="edge"> +<title>mconfig->config</title> +<path fill="none" stroke="#000000" d="M96.5403,-148.1119C131.668,-145.6591 193.1644,-141.3651 232.7073,-138.604"/> +<polygon fill="#000000" stroke="#000000" points="233.0159,-142.0911 242.7477,-137.9029 232.5282,-135.1081 233.0159,-142.0911"/> +<text text-anchor="middle" x="176.5" y="-147.8" font-family="Times,serif" font-size="14.00" fill="#000000">image id</text> +</g> +<!-- layers --> +<g id="node2" class="node"> +<title>layers</title> +<polygon fill="none" stroke="#000000" points="96.5,-222 42.5,-222 42.5,-186 96.5,-186 96.5,-222"/> +<text text-anchor="middle" x="69.5" y="-200.3" font-family="Times,serif" font-size="14.00" fill="#000000">Layers</text> +</g> +<!-- l1 --> +<g id="node6" class="node"> +<title>l1</title> +<polygon fill="none" stroke="#000000" points="479,-90 476,-94 455,-94 452,-90 417,-90 417,-54 479,-54 479,-90"/> +<text text-anchor="middle" x="448" y="-68.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text> +</g> +<!-- layers->l1 --> +<g id="edge2" class="edge"> +<title>layers->l1</title> +<path fill="none" stroke="#000000" d="M96.5198,-204.19C165.9738,-204.4755 346.9131,-203.8632 370,-190 390.008,-177.9856 385.5908,-163.8126 394.84,-142.0701"/> +<polygon fill="#000000" stroke="#000000" points="398.031,-143.513 399.3348,-132.9987 391.7588,-140.4051 398.031,-143.513"/> +<text text-anchor="middle" x="270" y="-205.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text> +</g> +<!-- l2 --> +<g id="node7" class="node"> +<title>l2</title> +<polygon fill="none" stroke="#000000" points="479,-197 476,-201 455,-201 452,-197 417,-197 417,-161 479,-161 479,-197"/> +<text text-anchor="middle" x="448" y="-175.3" font-family="Times,serif" font-size="14.00" fill="#000000">layer.tar</text> +</g> +<!-- layers->l2 --> +<g id="edge3" class="edge"> +<title>layers->l2</title> +<path fill="none" stroke="#000000" d="M96.7682,-219.0493C117.8784,-229.293 148.1958,-241 176.5,-241 176.5,-241 176.5,-241 354.5,-241 367.5727,-241 373.2434,-242.298 387.8589,-235.9183"/> +<polygon fill="#000000" stroke="#000000" points="389.5034,-239.0113 397.0026,-231.5271 386.473,-232.7012 389.5034,-239.0113"/> +<text text-anchor="middle" x="270" y="-244.8" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text> +</g> +<!-- sources --> +<g id="node3" class="node"> +<title>sources</title> +<polygon fill="none" stroke="#000000" points="115,-114 24,-114 24,-78 115,-78 115,-114"/> +<text text-anchor="middle" x="69.5" y="-92.3" font-family="Times,serif" font-size="14.00" fill="#000000">LayerSources</text> +</g> +<!-- sources->l1 --> +<g id="edge6" class="edge"> +<title>sources->l1</title> +<path fill="none" stroke="#000000" d="M106.626,-77.9421C112.1689,-75.0511 117.7734,-72.0159 123,-69 147.6108,-54.799 148.086,-34 176.5,-34 176.5,-34 176.5,-34 354.5,-34 369.9935,-34 374.3272,-34.0242 389,-39 396.5806,-41.5707 404.3097,-45.114 411.5701,-48.9291"/> +<polygon fill="#000000" stroke="#000000" points="410.0253,-52.0743 420.4696,-53.8542 413.4148,-45.9497 410.0253,-52.0743"/> +<text text-anchor="middle" x="270" y="-37.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +<!-- sources->l2 --> +<g id="edge7" class="edge"> +<title>sources->l2</title> +<path fill="none" stroke="#000000" d="M115.0969,-83.9218C174.9057,-70.173 282.5191,-52.6874 370,-78 379.5941,-80.7761 383.4596,-81.69 389,-90 400.7544,-107.6302 386.3753,-118.6669 397,-137 400.6773,-143.3452 405.699,-149.1408 411.1641,-154.2655"/> +<polygon fill="#000000" stroke="#000000" points="408.9395,-156.9685 418.807,-160.8264 413.4991,-151.6571 408.9395,-156.9685"/> +<text text-anchor="middle" x="270" y="-69.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +<!-- tags --> +<g id="node4" class="node"> +<title>tags</title> +<polygon fill="none" stroke="#000000" points="105,-60 34,-60 34,-24 105,-24 105,-60"/> +<text text-anchor="middle" x="69.5" y="-38.3" font-family="Times,serif" font-size="14.00" fill="#000000">RepoTags</text> +</g> +<!-- config->l1 --> +<g id="edge4" class="edge"> +<title>config->l1</title> +<path fill="none" stroke="#000000" d="M297.0344,-126.2798C326.3338,-115.7451 373.4204,-98.8151 407.2182,-86.6631"/> +<polygon fill="#000000" stroke="#000000" points="408.5826,-89.892 416.8086,-83.2149 406.2141,-83.3048 408.5826,-89.892"/> +<text text-anchor="middle" x="354.5" y="-113.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +<!-- config->l2 --> +<g id="edge5" class="edge"> +<title>config->l2</title> +<path fill="none" stroke="#000000" d="M297.0344,-142.5308C326.2081,-149.5784 373.0165,-160.886 406.7823,-169.0429"/> +<polygon fill="#000000" stroke="#000000" points="406.2663,-172.5189 416.8086,-171.465 407.9101,-165.7146 406.2663,-172.5189"/> +<text text-anchor="middle" x="354.5" y="-163.8" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</g> +</g> +</svg> diff --git a/images/upload.dot.svg b/images/upload.dot.svg new file mode 100644 index 0000000..16ba738 --- /dev/null +++ b/images/upload.dot.svg @@ -0,0 +1,359 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- Generated by graphviz version 2.40.1 (20161225.0304) + --> +<!-- Title: G Pages: 1 --> +<svg width="505pt" height="882pt" + viewBox="0.00 0.00 504.57 881.79" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 877.7939)"> +<title>G</title> +<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-877.7939 500.5651,-877.7939 500.5651,4 -4,4"/> +<!-- fs --> +<g id="node1" class="node"> +<title>fs</title> +<g id="a_node1"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/layer.md" xlink:title="filesystem\nchangeset"> +<polygon fill="none" stroke="#000000" points="291.9498,-873.7939 288.9498,-877.7939 267.9498,-877.7939 264.9498,-873.7939 218.9498,-873.7939 218.9498,-835.7939 291.9498,-835.7939 291.9498,-873.7939"/> +<text text-anchor="middle" x="255.4498" y="-858.5939" font-family="Times,serif" font-size="14.00" fill="#000000">filesystem</text> +<text text-anchor="middle" x="255.4498" y="-843.5939" font-family="Times,serif" font-size="14.00" fill="#000000">changeset</text> +</a> +</g> +</g> +<!-- tar --> +<g id="node3" class="node"> +<title>tar</title> +<polygon fill="none" stroke="#000000" points="282.4498,-799.7939 228.4498,-799.7939 228.4498,-763.7939 282.4498,-763.7939 282.4498,-799.7939"/> +<text text-anchor="middle" x="255.4498" y="-778.0939" font-family="Times,serif" font-size="14.00" fill="#000000">tar</text> +</g> +<!-- fs->tar --> +<g id="edge2" class="edge"> +<title>fs->tar</title> +<path fill="none" stroke="#000000" d="M255.4498,-835.614C255.4498,-827.8913 255.4498,-818.8221 255.4498,-810.3524"/> +<polygon fill="#000000" stroke="#000000" points="258.9499,-810.0912 255.4498,-800.0912 251.9499,-810.0912 258.9499,-810.0912"/> +</g> +<!-- configuration --> +<g id="node2" class="node"> +<title>configuration</title> +<g id="a_node2"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#properties" xlink:title="image\nconfig"> +<polygon fill="none" stroke="#000000" points="299.233,-552.767 278.3414,-583.8207 236.5583,-583.8207 215.6667,-552.767 236.5583,-521.7132 278.3414,-521.7132 299.233,-552.767"/> +<text text-anchor="middle" x="257.4498" y="-556.567" font-family="Times,serif" font-size="14.00" fill="#000000">image</text> +<text text-anchor="middle" x="257.4498" y="-541.567" font-family="Times,serif" font-size="14.00" fill="#000000">config</text> +</a> +</g> +</g> +<!-- config --> +<g id="node16" class="node"> +<title>config</title> +<g id="a_node16"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md" xlink:title="config file"> +<polygon fill="none" stroke="#000000" points="197.9498,-485.7401 130.9498,-485.7401 130.9498,-449.7401 203.9498,-449.7401 203.9498,-479.7401 197.9498,-485.7401"/> +<polyline fill="none" stroke="#000000" points="197.9498,-485.7401 197.9498,-479.7401 "/> +<polyline fill="none" stroke="#000000" points="203.9498,-479.7401 197.9498,-479.7401 "/> +<text text-anchor="middle" x="167.4498" y="-464.0401" font-family="Times,serif" font-size="14.00" fill="#000000">config file</text> +</a> +</g> +</g> +<!-- configuration->config --> +<g id="edge1" class="edge"> +<title>configuration->config</title> +<path fill="none" stroke="#000000" d="M231.9002,-528.6291C220.1342,-517.5133 206.1713,-504.322 194.2553,-493.0644"/> +<polygon fill="#000000" stroke="#000000" points="196.4367,-490.3103 186.764,-485.987 191.6295,-495.3986 196.4367,-490.3103"/> +</g> +<!-- tee --> +<g id="node5" class="node"> +<title>tee</title> +<polygon fill="none" stroke="#000000" points="282.4498,-727.7939 228.4498,-727.7939 228.4498,-691.7939 282.4498,-691.7939 282.4498,-727.7939"/> +<text text-anchor="middle" x="255.4498" y="-706.0939" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text> +</g> +<!-- tar->tee --> +<g id="edge3" class="edge"> +<title>tar->tee</title> +<path fill="none" stroke="#000000" d="M255.4498,-763.6252C255.4498,-755.9248 255.4498,-746.7682 255.4498,-738.2105"/> +<polygon fill="#000000" stroke="#000000" points="258.9499,-738.2071 255.4498,-728.2071 251.9499,-738.2072 258.9499,-738.2071"/> +</g> +<!-- gzip --> +<g id="node4" class="node"> +<title>gzip</title> +<polygon fill="none" stroke="#000000" points="347.4498,-655.7939 293.4498,-655.7939 293.4498,-619.7939 347.4498,-619.7939 347.4498,-655.7939"/> +<text text-anchor="middle" x="320.4498" y="-634.0939" font-family="Times,serif" font-size="14.00" fill="#000000">gzip</text> +</g> +<!-- layer --> +<g id="node17" class="node"> +<title>layer</title> +<g id="a_node17"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/layer.md" xlink:title="layer"> +<polygon fill="none" stroke="#000000" points="375.4498,-570.767 327.4498,-570.767 327.4498,-534.767 381.4498,-534.767 381.4498,-564.767 375.4498,-570.767"/> +<polyline fill="none" stroke="#000000" points="375.4498,-570.767 375.4498,-564.767 "/> +<polyline fill="none" stroke="#000000" points="381.4498,-564.767 375.4498,-564.767 "/> +<text text-anchor="middle" x="354.4498" y="-549.067" font-family="Times,serif" font-size="14.00" fill="#000000">layer</text> +</a> +</g> +</g> +<!-- gzip->layer --> +<g id="edge7" class="edge"> +<title>gzip->layer</title> +<path fill="none" stroke="#000000" d="M327.6604,-619.7618C332.2583,-608.2635 338.3055,-593.1407 343.4908,-580.1733"/> +<polygon fill="#000000" stroke="#000000" points="346.7866,-581.3577 347.2497,-570.773 340.2869,-578.7586 346.7866,-581.3577"/> +</g> +<!-- tee->gzip --> +<g id="edge6" class="edge"> +<title>tee->gzip</title> +<path fill="none" stroke="#000000" d="M271.8521,-691.6252C279.4914,-683.1632 288.7182,-672.9428 297.0705,-663.6909"/> +<polygon fill="#000000" stroke="#000000" points="299.7236,-665.9752 303.8267,-656.2071 294.5277,-661.2845 299.7236,-665.9752"/> +</g> +<!-- sha256sum --> +<g id="node8" class="node"> +<title>sha256sum</title> +<polygon fill="none" stroke="#000000" points="252.4498,-655.7939 174.4498,-655.7939 174.4498,-619.7939 252.4498,-619.7939 252.4498,-655.7939"/> +<text text-anchor="middle" x="213.4498" y="-634.0939" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text> +</g> +<!-- tee->sha256sum --> +<g id="edge4" class="edge"> +<title>tee->sha256sum</title> +<path fill="none" stroke="#000000" d="M244.8515,-691.6252C240.1621,-683.5863 234.547,-673.9604 229.37,-665.0856"/> +<polygon fill="#000000" stroke="#000000" points="232.2529,-663.0814 224.1909,-656.2071 226.2064,-666.6085 232.2529,-663.0814"/> +</g> +<!-- tee2 --> +<g id="node6" class="node"> +<title>tee2</title> +<polygon fill="none" stroke="#000000" points="407.4498,-413.7401 353.4498,-413.7401 353.4498,-377.7401 407.4498,-377.7401 407.4498,-413.7401"/> +<text text-anchor="middle" x="380.4498" y="-392.0401" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text> +</g> +<!-- sha256sum2 --> +<g id="node9" class="node"> +<title>sha256sum2</title> +<polygon fill="none" stroke="#000000" points="370.4498,-341.7401 292.4498,-341.7401 292.4498,-305.7401 370.4498,-305.7401 370.4498,-341.7401"/> +<text text-anchor="middle" x="331.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text> +</g> +<!-- tee2->sha256sum2 --> +<g id="edge9" class="edge"> +<title>tee2->sha256sum2</title> +<path fill="none" stroke="#000000" d="M368.0851,-377.5715C362.499,-369.3634 355.7869,-359.5007 349.6426,-350.4724"/> +<polygon fill="#000000" stroke="#000000" points="352.5009,-348.4513 343.9811,-342.1534 346.7139,-352.3897 352.5009,-348.4513"/> +</g> +<!-- curl --> +<g id="node11" class="node"> +<title>curl</title> +<polygon fill="none" stroke="#000000" points="476.4498,-180 422.4498,-180 422.4498,-144 476.4498,-144 476.4498,-180"/> +<text text-anchor="middle" x="449.4498" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text> +</g> +<!-- tee2->curl --> +<g id="edge14" class="edge"> +<title>tee2->curl</title> +<path fill="none" stroke="#000000" d="M407.4647,-382.2896C424.2217,-372.836 445.1987,-358.8424 459.4498,-341.7401 481.8672,-314.8378 484.1804,-303.9954 491.4498,-269.7401 496.408,-246.3759 499.2012,-238.5917 491.4498,-216 487.9808,-205.8894 481.7599,-196.2137 475.1508,-187.9166"/> +<polygon fill="#000000" stroke="#000000" points="477.6506,-185.4539 468.4998,-180.1141 472.3234,-189.9949 477.6506,-185.4539"/> +</g> +<!-- wc --> +<g id="node14" class="node"> +<title>wc</title> +<polygon fill="none" stroke="#000000" points="450.4498,-341.7401 396.4498,-341.7401 396.4498,-305.7401 450.4498,-305.7401 450.4498,-341.7401"/> +<text text-anchor="middle" x="423.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">wc -c</text> +</g> +<!-- tee2->wc --> +<g id="edge11" class="edge"> +<title>tee2->wc</title> +<path fill="none" stroke="#000000" d="M391.3005,-377.5715C396.1521,-369.448 401.9715,-359.7038 407.3178,-350.7519"/> +<polygon fill="#000000" stroke="#000000" points="410.3305,-352.5334 412.453,-342.1534 404.3207,-348.9442 410.3305,-352.5334"/> +</g> +<!-- tee3 --> +<g id="node7" class="node"> +<title>tee3</title> +<polygon fill="none" stroke="#000000" points="175.4498,-413.7401 121.4498,-413.7401 121.4498,-377.7401 175.4498,-377.7401 175.4498,-413.7401"/> +<text text-anchor="middle" x="148.4498" y="-392.0401" font-family="Times,serif" font-size="14.00" fill="#000000">tee</text> +</g> +<!-- sha256sum3 --> +<g id="node10" class="node"> +<title>sha256sum3</title> +<polygon fill="none" stroke="#000000" points="220.4498,-341.7401 142.4498,-341.7401 142.4498,-305.7401 220.4498,-305.7401 220.4498,-341.7401"/> +<text text-anchor="middle" x="181.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">sha256sum</text> +</g> +<!-- tee3->sha256sum3 --> +<g id="edge21" class="edge"> +<title>tee3->sha256sum3</title> +<path fill="none" stroke="#000000" d="M156.7771,-377.5715C160.4228,-369.6172 164.7807,-360.1091 168.8125,-351.3124"/> +<polygon fill="#000000" stroke="#000000" points="172.0255,-352.7024 173.0104,-342.1534 165.6621,-349.7857 172.0255,-352.7024"/> +</g> +<!-- curl2 --> +<g id="node12" class="node"> +<title>curl2</title> +<polygon fill="none" stroke="#000000" points="140.4498,-108 86.4498,-108 86.4498,-72 140.4498,-72 140.4498,-108"/> +<text text-anchor="middle" x="113.4498" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text> +</g> +<!-- tee3->curl2 --> +<g id="edge18" class="edge"> +<title>tee3->curl2</title> +<path fill="none" stroke="#000000" d="M121.176,-387.9084C97.0472,-379.7786 62.2297,-364.9521 39.4498,-341.7401 14.6626,-316.4827 13.193,-304.2711 5.4498,-269.7401 .2238,-246.4344 -2.8368,-238.4009 5.4498,-216 20.6686,-174.8598 55.9849,-138.0878 82.1108,-115.0395"/> +<polygon fill="#000000" stroke="#000000" points="84.5989,-117.5158 89.8914,-108.3375 80.0304,-112.2121 84.5989,-117.5158"/> +</g> +<!-- wc2 --> +<g id="node15" class="node"> +<title>wc2</title> +<polygon fill="none" stroke="#000000" points="102.4498,-341.7401 48.4498,-341.7401 48.4498,-305.7401 102.4498,-305.7401 102.4498,-341.7401"/> +<text text-anchor="middle" x="75.4498" y="-320.0401" font-family="Times,serif" font-size="14.00" fill="#000000">wc -c</text> +</g> +<!-- tee3->wc2 --> +<g id="edge20" class="edge"> +<title>tee3->wc2</title> +<path fill="none" stroke="#000000" d="M130.0288,-377.5715C121.2969,-368.9591 110.7181,-358.5252 101.2075,-349.1449"/> +<polygon fill="#000000" stroke="#000000" points="103.4424,-346.4332 93.8649,-341.9029 98.5269,-351.417 103.4424,-346.4332"/> +</g> +<!-- diffid --> +<g id="node24" class="node"> +<title>diffid</title> +<g id="a_node24"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#layer-diffid" xlink:title="diffid"> +<ellipse fill="none" stroke="#000000" cx="167.4498" cy="-552.767" rx="30.5947" ry="18"/> +<text text-anchor="middle" x="167.4498" y="-549.067" font-family="Times,serif" font-size="14.00" fill="#000000">diffid</text> +</a> +</g> +</g> +<!-- sha256sum->diffid --> +<g id="edge5" class="edge"> +<title>sha256sum->diffid</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M203.6944,-619.7618C197.3279,-607.9938 188.9074,-592.4294 181.7852,-579.2647"/> +<polygon fill="#000000" stroke="#000000" points="184.7133,-577.3214 176.8766,-570.1915 178.5566,-580.6523 184.7133,-577.3214"/> +</g> +<!-- layer_digest --> +<g id="node23" class="node"> +<title>layer_digest</title> +<ellipse fill="none" stroke="#000000" cx="324.4498" cy="-242.8701" rx="51.9908" ry="18"/> +<text text-anchor="middle" x="324.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">layer digest</text> +</g> +<!-- sha256sum2->layer_digest --> +<g id="edge10" class="edge"> +<title>sha256sum2->layer_digest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M329.8614,-305.3894C328.9782,-295.1851 327.8606,-282.274 326.8707,-270.8377"/> +<polygon fill="#000000" stroke="#000000" points="330.3573,-270.5304 326.0078,-260.8695 323.3833,-271.1341 330.3573,-270.5304"/> +</g> +<!-- config_digest --> +<g id="node22" class="node"> +<title>config_digest</title> +<g id="a_node22"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/config.md#imageid" xlink:title="config digest\n(image id)"> +<ellipse fill="none" stroke="#000000" cx="192.4498" cy="-242.8701" rx="61.5366" ry="26.7407"/> +<text text-anchor="middle" x="192.4498" y="-246.6701" font-family="Times,serif" font-size="14.00" fill="#000000">config digest</text> +<text text-anchor="middle" x="192.4498" y="-231.6701" font-family="Times,serif" font-size="14.00" fill="#000000">(image id)</text> +</a> +</g> +</g> +<!-- sha256sum3->config_digest --> +<g id="edge23" class="edge"> +<title>sha256sum3->config_digest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M183.9459,-305.3894C184.9756,-297.8196 186.2078,-288.7601 187.4105,-279.9182"/> +<polygon fill="#000000" stroke="#000000" points="190.9151,-280.1212 188.7949,-269.7407 183.9789,-279.1777 190.9151,-280.1212"/> +</g> +<!-- registry --> +<g id="node19" class="node"> +<title>registry</title> +<g id="a_node19"><a xlink:href="https://github.com/opencontainers/distribution-spec/blob/master/spec.md" xlink:title="registry"> +<path fill="none" stroke="#000000" d="M281.4498,-32.7273C281.4498,-34.5331 268.4516,-36 252.4498,-36 236.4481,-36 223.4498,-34.5331 223.4498,-32.7273 223.4498,-32.7273 223.4498,-3.2727 223.4498,-3.2727 223.4498,-1.4669 236.4481,0 252.4498,0 268.4516,0 281.4498,-1.4669 281.4498,-3.2727 281.4498,-3.2727 281.4498,-32.7273 281.4498,-32.7273"/> +<path fill="none" stroke="#000000" d="M281.4498,-32.7273C281.4498,-30.9214 268.4516,-29.4545 252.4498,-29.4545 236.4481,-29.4545 223.4498,-30.9214 223.4498,-32.7273"/> +<text text-anchor="middle" x="252.4498" y="-14.3" font-family="Times,serif" font-size="14.00" fill="#000000">registry</text> +</a> +</g> +</g> +<!-- curl->registry --> +<g id="edge15" class="edge"> +<title>curl->registry</title> +<path fill="none" stroke="#000000" d="M424.4996,-143.7623C388.8852,-117.7294 323.4665,-69.9107 284.3667,-41.3301"/> +<polygon fill="#000000" stroke="#000000" points="286.1098,-38.2689 275.9712,-35.1933 281.9789,-43.9201 286.1098,-38.2689"/> +</g> +<!-- curl2->registry --> +<g id="edge19" class="edge"> +<title>curl2->registry</title> +<path fill="none" stroke="#000000" d="M140.5804,-75.9468C161.5049,-65.1082 190.7151,-49.9777 214.0402,-37.8956"/> +<polygon fill="#000000" stroke="#000000" points="215.7151,-40.9698 222.9848,-33.2625 212.4955,-34.7541 215.7151,-40.9698"/> +</g> +<!-- curl3 --> +<g id="node13" class="node"> +<title>curl3</title> +<polygon fill="none" stroke="#000000" points="279.4498,-108 225.4498,-108 225.4498,-72 279.4498,-72 279.4498,-108"/> +<text text-anchor="middle" x="252.4498" y="-86.3" font-family="Times,serif" font-size="14.00" fill="#000000">curl</text> +</g> +<!-- curl3->registry --> +<g id="edge28" class="edge"> +<title>curl3->registry</title> +<path fill="none" stroke="#000000" d="M252.4498,-71.8314C252.4498,-64.131 252.4498,-54.9743 252.4498,-46.4166"/> +<polygon fill="#000000" stroke="#000000" points="255.9499,-46.4132 252.4498,-36.4133 248.9499,-46.4133 255.9499,-46.4132"/> +</g> +<!-- layer_size --> +<g id="node21" class="node"> +<title>layer_size</title> +<ellipse fill="none" stroke="#000000" cx="438.4498" cy="-242.8701" rx="44.393" ry="18"/> +<text text-anchor="middle" x="438.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">layer size</text> +</g> +<!-- wc->layer_size --> +<g id="edge12" class="edge"> +<title>wc->layer_size</title> +<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M426.8536,-305.3894C428.7463,-295.1851 431.1411,-282.274 433.2623,-270.8377"/> +<polygon fill="#000000" stroke="#000000" points="436.7287,-271.3402 435.1112,-260.8695 429.8461,-270.0635 436.7287,-271.3402"/> +</g> +<!-- config_size --> +<g id="node20" class="node"> +<title>config_size</title> +<ellipse fill="none" stroke="#000000" cx="63.4498" cy="-242.8701" rx="49.2915" ry="18"/> +<text text-anchor="middle" x="63.4498" y="-239.1701" font-family="Times,serif" font-size="14.00" fill="#000000">config size</text> +</g> +<!-- wc2->config_size --> +<g id="edge22" class="edge"> +<title>wc2->config_size</title> +<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M72.7268,-305.3894C71.2127,-295.1851 69.2968,-282.274 67.5998,-270.8377"/> +<polygon fill="#000000" stroke="#000000" points="71.0507,-270.2475 66.1207,-260.8695 64.1265,-271.275 71.0507,-270.2475"/> +</g> +<!-- config->tee3 --> +<g id="edge17" class="edge"> +<title>config->tee3</title> +<path fill="none" stroke="#000000" d="M162.6553,-449.5715C160.6009,-441.7865 158.1538,-432.513 155.8743,-423.8748"/> +<polygon fill="#000000" stroke="#000000" points="159.2447,-422.9293 153.3089,-414.1534 152.4764,-424.7155 159.2447,-422.9293"/> +</g> +<!-- layer->tee2 --> +<g id="edge8" class="edge"> +<title>layer->tee2</title> +<path fill="none" stroke="#000000" d="M357.4324,-534.754C361.9143,-507.6851 370.4272,-456.272 375.7497,-424.1265"/> +<polygon fill="#000000" stroke="#000000" points="379.2414,-424.4641 377.422,-414.0267 372.3354,-423.3206 379.2414,-424.4641"/> +</g> +<!-- manifest --> +<g id="node18" class="node"> +<title>manifest</title> +<g id="a_node18"><a xlink:href="https://github.com/opencontainers/image-spec/blob/master/manifest.md" xlink:title="manifest"> +<polygon fill="none" stroke="#000000" points="278.4498,-180 220.4498,-180 220.4498,-144 284.4498,-144 284.4498,-174 278.4498,-180"/> +<polyline fill="none" stroke="#000000" points="278.4498,-180 278.4498,-174 "/> +<polyline fill="none" stroke="#000000" points="284.4498,-174 278.4498,-174 "/> +<text text-anchor="middle" x="252.4498" y="-158.3" font-family="Times,serif" font-size="14.00" fill="#000000">manifest</text> +</a> +</g> +</g> +<!-- manifest->curl3 --> +<g id="edge27" class="edge"> +<title>manifest->curl3</title> +<path fill="none" stroke="#000000" d="M252.4498,-143.8314C252.4498,-136.131 252.4498,-126.9743 252.4498,-118.4166"/> +<polygon fill="#000000" stroke="#000000" points="255.9499,-118.4132 252.4498,-108.4133 248.9499,-118.4133 255.9499,-118.4132"/> +</g> +<!-- config_size->manifest --> +<g id="edge25" class="edge"> +<title>config_size->manifest</title> +<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M93.7415,-228.5389C102.634,-224.4127 112.4011,-219.9589 121.4498,-216 151.126,-203.0165 184.9324,-189.1026 210.7685,-178.6506"/> +<polygon fill="#000000" stroke="#000000" points="212.2908,-181.8106 220.255,-174.8234 209.6718,-175.319 212.2908,-181.8106"/> +</g> +<!-- layer_size->manifest --> +<g id="edge13" class="edge"> +<title>layer_size->manifest</title> +<path fill="none" stroke="#000000" stroke-dasharray="1,5" d="M411.4267,-228.6221C403.1596,-224.412 393.9931,-219.8918 385.4498,-216 355.1525,-202.1984 320.2569,-188.0959 293.8131,-177.765"/> +<polygon fill="#000000" stroke="#000000" points="295.0659,-174.4969 284.4772,-174.1371 292.5304,-181.0216 295.0659,-174.4969"/> +</g> +<!-- config_digest->manifest --> +<g id="edge24" class="edge"> +<title>config_digest->manifest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M211.4342,-217.2823C218.2685,-208.0708 225.9885,-197.6656 232.8723,-188.3872"/> +<polygon fill="#000000" stroke="#000000" points="235.7622,-190.3661 238.9098,-180.2497 230.1405,-186.1952 235.7622,-190.3661"/> +</g> +<!-- layer_digest->manifest --> +<g id="edge26" class="edge"> +<title>layer_digest->manifest</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M308.8269,-225.3225C299.0559,-214.3477 286.2986,-200.0188 275.3913,-187.7678"/> +<polygon fill="#000000" stroke="#000000" points="277.8045,-185.2148 268.5408,-180.0733 272.5764,-189.8695 277.8045,-185.2148"/> +</g> +<!-- diffid->config --> +<g id="edge16" class="edge"> +<title>diffid->config</title> +<path fill="none" stroke="#000000" stroke-dasharray="5,2" d="M167.4498,-534.735C167.4498,-523.462 167.4498,-508.7054 167.4498,-495.9117"/> +<polygon fill="#000000" stroke="#000000" points="170.9499,-495.7461 167.4498,-485.7461 163.9499,-495.7462 170.9499,-495.7461"/> +</g> +</g> +</svg> diff --git a/internal/and/and_closer.go b/internal/and/and_closer.go new file mode 100644 index 0000000..14a05ea --- /dev/null +++ b/internal/and/and_closer.go @@ -0,0 +1,48 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package and provides helpers for adding Close to io.{Reader|Writer}. +package and + +import ( + "io" +) + +// ReadCloser implements io.ReadCloser by reading from a particular io.Reader +// and then calling the provided "Close()" method. +type ReadCloser struct { + io.Reader + CloseFunc func() error +} + +var _ io.ReadCloser = (*ReadCloser)(nil) + +// Close implements io.ReadCloser +func (rac *ReadCloser) Close() error { + return rac.CloseFunc() +} + +// WriteCloser implements io.WriteCloser by reading from a particular io.Writer +// and then calling the provided "Close()" method. +type WriteCloser struct { + io.Writer + CloseFunc func() error +} + +var _ io.WriteCloser = (*WriteCloser)(nil) + +// Close implements io.WriteCloser +func (wac *WriteCloser) Close() error { + return wac.CloseFunc() +} diff --git a/internal/and/and_closer_test.go b/internal/and/and_closer_test.go new file mode 100644 index 0000000..947ceae --- /dev/null +++ b/internal/and/and_closer_test.go @@ -0,0 +1,85 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package and + +import ( + "bytes" + "io" + "testing" +) + +func TestRead(t *testing.T) { + want := "asdf" + r := bytes.NewBufferString(want) + called := false + + rac := &ReadCloser{ + Reader: r, + CloseFunc: func() error { + called = true + return nil + }, + } + + data, err := io.ReadAll(rac) + if err != nil { + t.Errorf("ReadAll(rac) = %v", err) + } + if got := string(data); got != want { + t.Errorf("ReadAll(rac); got %q, want %q", got, want) + } + + if called { + t.Error("called before Close(); got true, wanted false") + } + if err := rac.Close(); err != nil { + t.Errorf("Close() = %v", err) + } + if !called { + t.Error("called after Close(); got false, wanted true") + } +} + +func TestWrite(t *testing.T) { + w := bytes.NewBuffer([]byte{}) + called := false + + wac := &WriteCloser{ + Writer: w, + CloseFunc: func() error { + called = true + return nil + }, + } + + want := "asdf" + if _, err := wac.Write([]byte(want)); err != nil { + t.Errorf("Write(%q); = %v", want, err) + } + + if called { + t.Error("called before Close(); got true, wanted false") + } + if err := wac.Close(); err != nil { + t.Errorf("Close() = %v", err) + } + if !called { + t.Error("called after Close(); got false, wanted true") + } + + if got := w.String(); got != want { + t.Errorf("w.String(); got %q, want %q", got, want) + } +} diff --git a/internal/cmd/edit.go b/internal/cmd/edit.go new file mode 100644 index 0000000..707e80c --- /dev/null +++ b/internal/cmd/edit.go @@ -0,0 +1,485 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/google/go-containerregistry/internal/editor" + "github.com/google/go-containerregistry/internal/verify" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/spf13/cobra" +) + +// NewCmdEdit creates a new cobra.Command for the edit subcommand. +// +// This is currently hidden until we're happy with the interface and can test +// it on different operating systems and editors. +func NewCmdEdit(options *[]crane.Option) *cobra.Command { + cmd := &cobra.Command{ + Hidden: true, + Use: "edit", + Short: "Edit the contents of an image.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, _ []string) { + cmd.Usage() + }, + } + cmd.AddCommand(NewCmdEditManifest(options), NewCmdEditConfig(options), NewCmdEditFs(options)) + + return cmd +} + +// NewCmdConfig creates a new cobra.Command for the config subcommand. +func NewCmdEditConfig(options *[]crane.Option) *cobra.Command { + var dst string + cmd := &cobra.Command{ + Use: "config", + Short: "Edit an image's config file.", + Example: ` # Edit ubuntu's config file + crane edit config ubuntu + + # Overwrite ubuntu's config file with '{}' + echo '{}' | crane edit config ubuntu`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ref, err := editConfig(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, *options...) + if err != nil { + return fmt.Errorf("editing config: %w", err) + } + fmt.Println(ref.String()) + return nil + }, + } + cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.") + + return cmd +} + +// NewCmdManifest creates a new cobra.Command for the manifest subcommand. +func NewCmdEditManifest(options *[]crane.Option) *cobra.Command { + var ( + dst string + mt string + ) + cmd := &cobra.Command{ + Use: "manifest", + Short: "Edit an image's manifest.", + Example: ` # Edit ubuntu's manifest + crane edit manifest ubuntu`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ref, err := editManifest(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], dst, mt, *options...) + if err != nil { + return fmt.Errorf("editing manifest: %w", err) + } + fmt.Println(ref.String()) + return nil + }, + } + cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.") + cmd.Flags().StringVarP(&mt, "media-type", "m", "", "Override the mediaType used as the Content-Type for PUT") + + return cmd +} + +// NewCmdExport creates a new cobra.Command for the export subcommand. +func NewCmdEditFs(options *[]crane.Option) *cobra.Command { + var dst, name string + cmd := &cobra.Command{ + Use: "fs IMAGE", + Short: "Edit the contents of an image's filesystem.", + Example: ` # Edit motd-news using $EDITOR + crane edit fs ubuntu -f /etc/default/motd-news + + # Overwrite motd-news with 'ENABLED=0' + echo 'ENABLED=0' | crane edit fs ubuntu -f /etc/default/motd-news`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ref, err := editFile(cmd.InOrStdin(), cmd.OutOrStdout(), args[0], name, dst, *options...) + if err != nil { + return fmt.Errorf("editing file: %w", err) + } + fmt.Println(ref.String()) + return nil + }, + } + cmd.Flags().StringVarP(&name, "filename", "f", "", "Edit the given filename") + cmd.Flags().StringVarP(&dst, "tag", "t", "", "New tag reference to apply to mutated image. If not provided, uses original tag or pushes a new digest.") + cmd.MarkFlagRequired("filename") + + return cmd +} + +func interactive(in io.Reader, out io.Writer) bool { + return interactiveFile(in) && interactiveFile(out) +} + +func interactiveFile(i any) bool { + f, ok := i.(*os.File) + if !ok { + return false + } + stat, err := f.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) != 0 +} + +func editConfig(in io.Reader, out io.Writer, src, dst string, options ...crane.Option) (name.Reference, error) { + o := crane.GetOptions(options...) + + img, err := crane.Pull(src, options...) + if err != nil { + return nil, err + } + + mt, err := img.MediaType() + if err != nil { + return nil, err + } + + // We want to omit Layers in certain situations, so we don't use v1.Image.Manifest() here. + // Instead, we treat the manifest as a map[string]any and just manipulate the config desc. + mb, err := img.RawManifest() + if err != nil { + return nil, err + } + + jsonMap := map[string]any{} + if err := json.Unmarshal(mb, &jsonMap); err != nil { + return nil, err + } + + cv, ok := jsonMap["config"] + if !ok { + return nil, fmt.Errorf("config missing") + } + cb, err := json.Marshal(cv) + if err != nil { + return nil, fmt.Errorf("json.Marshal config: %w", err) + } + + config := v1.Descriptor{} + if err := json.Unmarshal(cb, &config); err != nil { + return nil, fmt.Errorf("json.Unmarshal config: %w", err) + } + + var edited []byte + if interactive(in, out) { + rcf, err := img.RawConfigFile() + if err != nil { + return nil, err + } + edited, err = editor.Edit(bytes.NewReader(rcf), ".json") + if err != nil { + return nil, err + } + } else { + b, err := io.ReadAll(in) + if err != nil { + return nil, err + } + edited = b + } + + // this has to happen before we modify the descriptor (so we can use verify.Descriptor to validate whether m.Config.Data matches m.Config.Digest/Size) + if config.Data != nil && verify.Descriptor(config) == nil { + // https://github.com/google/go-containerregistry/issues/1552#issuecomment-1452653875 + // "if data is non-empty and correct, we should update it" + config.Data = edited + } + + l := static.NewLayer(edited, config.MediaType) + layerDigest, err := l.Digest() + if err != nil { + return nil, err + } + + config.Digest = layerDigest + config.Size = int64(len(edited)) + + jsonMap["config"] = config + b, err := json.Marshal(jsonMap) + if err != nil { + return nil, err + } + rm := &rawManifest{ + body: b, + mediaType: mt, + } + + digest, _, _ := v1.SHA256(bytes.NewReader(b)) + + if dst == "" { + dst = src + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, err + } + if _, ok := ref.(name.Digest); ok { + dst = ref.Context().Digest(digest.String()).String() + } + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return nil, err + } + + if err := remote.WriteLayer(dstRef.Context(), l, o.Remote...); err != nil { + return nil, err + } + + if err := remote.Put(dstRef, rm, o.Remote...); err != nil { + return nil, err + } + + return dstRef, nil +} + +func editManifest(in io.Reader, out io.Writer, src string, dst string, mt string, options ...crane.Option) (name.Reference, error) { + o := crane.GetOptions(options...) + + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, err + } + + desc, err := remote.Get(ref, o.Remote...) + if err != nil { + return nil, err + } + + var edited []byte + if interactive(in, out) { + edited, err = editor.Edit(bytes.NewReader(desc.Manifest), ".json") + if err != nil { + return nil, err + } + } else { + b, err := io.ReadAll(in) + if err != nil { + return nil, err + } + edited = b + } + + digest, _, err := v1.SHA256(bytes.NewReader(edited)) + if err != nil { + return nil, err + } + + if dst == "" { + dst = src + if _, ok := ref.(name.Digest); ok { + dst = ref.Context().Digest(digest.String()).String() + } + } + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return nil, err + } + + if mt == "" { + // If --media-type is unset, use Content-Type by default. + mt = string(desc.MediaType) + + // If document contains mediaType, default to that. + wmt := withMediaType{} + if err := json.Unmarshal(edited, &wmt); err == nil { + if wmt.MediaType != "" { + mt = wmt.MediaType + } + } + } + + rm := &rawManifest{ + body: edited, + mediaType: types.MediaType(mt), + } + + if err := remote.Put(dstRef, rm, o.Remote...); err != nil { + return nil, err + } + + return dstRef, nil +} + +func editFile(in io.Reader, out io.Writer, src, file, dst string, options ...crane.Option) (name.Reference, error) { + o := crane.GetOptions(options...) + + img, err := crane.Pull(src, options...) + if err != nil { + return nil, err + } + + // If stdin has content, read it in and use that for the file. + // Otherwise, scran through the image and open that file in an editor. + var ( + edited []byte + header *tar.Header + ) + if interactive(in, out) { + f, h, err := findFile(img, file) + if err != nil { + return nil, err + } + ext := filepath.Ext(h.Name) + if strings.Contains(ext, "..") { + return nil, fmt.Errorf("this is impossible but this check satisfies CWE-22 for file name %q", h.Name) + } + edited, err = editor.Edit(f, ext) + if err != nil { + return nil, err + } + header = h + } else { + b, err := io.ReadAll(in) + if err != nil { + return nil, err + } + edited = b + header = blankHeader(file) + } + + buf := bytes.NewBuffer(nil) + buf.Grow(len(edited)) + tw := tar.NewWriter(buf) + + header.Size = int64(len(edited)) + if err := tw.WriteHeader(header); err != nil { + return nil, err + } + if _, err := io.Copy(tw, bytes.NewReader(edited)); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + fileBytes := buf.Bytes() + fileLayer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(fileBytes)), nil + }) + if err != nil { + return nil, err + } + img, err = mutate.Append(img, mutate.Addendum{ + Layer: fileLayer, + History: v1.History{ + Author: "crane", + CreatedBy: strings.Join(os.Args, " "), + }, + }) + if err != nil { + return nil, err + } + + digest, err := img.Digest() + if err != nil { + return nil, err + } + + if dst == "" { + dst = src + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, err + } + if _, ok := ref.(name.Digest); ok { + dst = ref.Context().Digest(digest.String()).String() + } + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return nil, err + } + + if err := crane.Push(img, dst, options...); err != nil { + return nil, err + } + + return dstRef, nil +} + +func findFile(img v1.Image, name string) (io.Reader, *tar.Header, error) { + name = normalize(name) + tr := tar.NewReader(mutate.Extract(img)) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, nil, fmt.Errorf("reading tar: %w", err) + } + if normalize(header.Name) == name { + return tr, header, nil + } + } + + // If we don't find the file, we should create a new one. + return bytes.NewBufferString(""), blankHeader(name), nil +} + +func blankHeader(name string) *tar.Header { + return &tar.Header{ + Name: name, + Typeflag: tar.TypeReg, + // Use a fixed Mode, so that this isn't sensitive to the directory and umask + // under which it was created. Additionally, windows can only set 0222, + // 0444, or 0666, none of which are executable. + Mode: 0555, + } +} + +func normalize(name string) string { + return filepath.Clean("/" + name) +} + +type withMediaType struct { + MediaType string `json:"mediaType,omitempty"` +} + +type rawManifest struct { + body []byte + mediaType types.MediaType +} + +func (r *rawManifest) RawManifest() ([]byte, error) { + return r.body, nil +} + +func (r *rawManifest) MediaType() (types.MediaType, error) { + return r.mediaType, nil +} diff --git a/internal/cmd/edit_test.go b/internal/cmd/edit_test.go new file mode 100644 index 0000000..df18c45 --- /dev/null +++ b/internal/cmd/edit_test.go @@ -0,0 +1,174 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "bytes" + "io" + "net/http/httptest" + "net/url" + "path" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" +) + +func mustRegistry(t *testing.T) (*httptest.Server, string) { + t.Helper() + s := httptest.NewServer(registry.New()) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + return s, u.Host +} + +func TestEditConfig(t *testing.T) { + reg, host := mustRegistry(t) + defer reg.Close() + src := path.Join(host, "crane/edit/config") + + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + if err := crane.Push(img, src); err != nil { + t.Fatal(err) + } + + cmd := NewCmdEditConfig(&[]crane.Option{}) + cmd.SetArgs([]string{src}) + cmd.SetIn(strings.NewReader("{}")) + + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } +} + +func TestEditManifest(t *testing.T) { + reg, host := mustRegistry(t) + defer reg.Close() + src := path.Join(host, "crane/edit/manifest") + + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + if err := crane.Push(img, src); err != nil { + t.Fatal(err) + } + + cmd := NewCmdEditManifest(&[]crane.Option{}) + cmd.SetArgs([]string{src}) + cmd.SetIn(strings.NewReader("{}")) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } +} + +func TestEditFilesystem(t *testing.T) { + reg, host := mustRegistry(t) + defer reg.Close() + src := path.Join(host, "crane/edit/fs") + + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + if err := crane.Push(img, src); err != nil { + t.Fatal(err) + } + + cmd := NewCmdEditFs(&[]crane.Option{}) + cmd.SetArgs([]string{src}) + cmd.Flags().Set("filename", "/foo/bar") + cmd.SetIn(strings.NewReader("baz")) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + img, err = crane.Pull(src) + if err != nil { + t.Fatal(err) + } + + r, _, err := findFile(img, "/foo/bar") + if err != nil { + t.Fatal(err) + } + + got, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, []byte("baz")) { + t.Fatalf("got: %s, want %s", got, "baz") + } + + // Edit the same file to make sure we can edit existing files. + cmd = NewCmdEditFs(&[]crane.Option{}) + cmd.SetArgs([]string{src}) + cmd.Flags().Set("filename", "/foo/bar") + cmd.SetIn(strings.NewReader("quux")) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + + img, err = crane.Pull(src) + if err != nil { + t.Fatal(err) + } + + r, _, err = findFile(img, "/foo/bar") + if err != nil { + t.Fatal(err) + } + + got, err = io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(got, []byte("quux")) { + t.Fatalf("got: %s, want %s", got, "quux") + } +} + +func TestFindFile(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + r, h, err := findFile(img, "/does-not-exist") + if err != nil { + t.Fatal(err) + } + + b, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + if len(b) != 0 { + t.Errorf("expected empty reader, got: %s", string(b)) + } + + if h.Name != "/does-not-exist" { + t.Errorf("tar.Header has wrong name: %v", h) + } +} diff --git a/internal/compare/doc.go b/internal/compare/doc.go new file mode 100644 index 0000000..c8ca497 --- /dev/null +++ b/internal/compare/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package compare provides methods for comparing images, indexes, and layers. +package compare diff --git a/internal/compare/image.go b/internal/compare/image.go new file mode 100644 index 0000000..fe5fd4c --- /dev/null +++ b/internal/compare/image.go @@ -0,0 +1,111 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "errors" + "fmt" + "reflect" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Images compares the given images to each other and returns an error if they +// differ. +func Images(a, b v1.Image) error { + digests := []v1.Hash{} + manifests := []*v1.Manifest{} + cns := []v1.Hash{} + sizes := []int64{} + mts := []types.MediaType{} + layerss := [][]v1.Layer{} + + errs := []string{} + + for _, img := range []v1.Image{a, b} { + layers, err := img.Layers() + if err != nil { + return err + } + layerss = append(layerss, layers) + + digest, err := img.Digest() + if err != nil { + return err + } + digests = append(digests, digest) + + manifest, err := img.Manifest() + if err != nil { + return err + } + manifests = append(manifests, manifest) + + cn, err := img.ConfigName() + if err != nil { + return err + } + cns = append(cns, cn) + + size, err := img.Size() + if err != nil { + return err + } + sizes = append(sizes, size) + + mt, err := img.MediaType() + if err != nil { + return err + } + mts = append(mts, mt) + } + + if want, got := digests[0], digests[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got)) + } + if want, got := cns[0], cns[1]; want != got { + errs = append(errs, fmt.Sprintf("a.ConfigName() != b.ConfigName(); %s != %s", want, got)) + } + if want, got := manifests[0], manifests[1]; !reflect.DeepEqual(want, got) { + errs = append(errs, fmt.Sprintf("a.Manifest() != b.Manifest(); %v != %v", want, got)) + } + if want, got := sizes[0], sizes[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got)) + } + if want, got := mts[0], mts[1]; want != got { + errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got)) + } + + if len(layerss[0]) != len(layerss[1]) { + // If we have fewer layers than the first image, abort with an error so we don't panic. + return errors.New("len(a.Layers()) != len(b.Layers())") + } + + // Compare each layer. + for i := 0; i < len(layerss[0]); i++ { + if err := Layers(layerss[0][i], layerss[1][i]); err != nil { + // Wrap the error in newlines to delineate layer errors. + errs = append(errs, fmt.Sprintf("Layers[%d]: %v\n", i, err)) + } + } + + if len(errs) != 0 { + return errors.New("Images differ:\n" + strings.Join(errs, "\n")) + } + + return nil +} diff --git a/internal/compare/image_test.go b/internal/compare/image_test.go new file mode 100644 index 0000000..fe011a4 --- /dev/null +++ b/internal/compare/image_test.go @@ -0,0 +1,66 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestDifferentImages(t *testing.T) { + a, err := random.Image(100, 3) + if err != nil { + t.Fatal(err) + } + b, err := random.Image(100, 3) + if err != nil { + t.Fatal(err) + } + + b = mutate.MediaType(b, types.OCIManifestSchema1) + + if err := Images(a, b); err == nil { + t.Errorf("got nil err, should have something") + } +} + +func TestMismatchedLayers(t *testing.T) { + a, err := random.Image(100, 3) + if err != nil { + t.Fatal(err) + } + b, err := random.Image(100, 2) + if err != nil { + t.Fatal(err) + } + + if err := Images(a, b); err == nil { + t.Errorf("got nil err, should have something") + } +} + +func TestEqualImages(t *testing.T) { + a, err := random.Image(100, 2) + if err != nil { + t.Fatal(err) + } + + if err := Images(a, a); err != nil { + t.Errorf("got err: %v", err) + } +} diff --git a/internal/compare/index.go b/internal/compare/index.go new file mode 100644 index 0000000..c19620b --- /dev/null +++ b/internal/compare/index.go @@ -0,0 +1,83 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "errors" + "fmt" + "reflect" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Indexes compares the given indexes to each other and returns an error if +// they differ. +func Indexes(a, b v1.ImageIndex) error { + digests := []v1.Hash{} + manifests := []*v1.IndexManifest{} + sizes := []int64{} + mts := []types.MediaType{} + + errs := []string{} + + for _, idx := range []v1.ImageIndex{a, b} { + digest, err := idx.Digest() + if err != nil { + return err + } + digests = append(digests, digest) + + manifest, err := idx.IndexManifest() + if err != nil { + return err + } + manifests = append(manifests, manifest) + + size, err := idx.Size() + if err != nil { + return err + } + sizes = append(sizes, size) + + mt, err := idx.MediaType() + if err != nil { + return err + } + mts = append(mts, mt) + } + + if want, got := digests[0], digests[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got)) + } + if want, got := manifests[0], manifests[1]; !reflect.DeepEqual(want, got) { + errs = append(errs, fmt.Sprintf("a.Manifest() != b.Manifest(); %v != %v", want, got)) + } + if want, got := sizes[0], sizes[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got)) + } + if want, got := mts[0], mts[1]; want != got { + errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got)) + } + + // TODO(jonjohnsonjr): Iterate over Manifest and compare Image and ImageIndex results. + + if len(errs) != 0 { + return errors.New("Indexes differ:\n" + strings.Join(errs, "\n")) + } + + return nil +} diff --git a/internal/compare/index_test.go b/internal/compare/index_test.go new file mode 100644 index 0000000..962bf22 --- /dev/null +++ b/internal/compare/index_test.go @@ -0,0 +1,51 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestDifferentIndexes(t *testing.T) { + a, err := random.Index(100, 3, 3) + if err != nil { + t.Fatal(err) + } + b, err := random.Index(100, 2, 2) + if err != nil { + t.Fatal(err) + } + + b = mutate.IndexMediaType(b, types.DockerManifestList) + + if err := Indexes(a, b); err == nil { + t.Errorf("got nil err, should have something") + } +} + +func TestEqualIndexes(t *testing.T) { + a, err := random.Index(100, 2, 2) + if err != nil { + t.Fatal(err) + } + + if err := Indexes(a, a); err != nil { + t.Errorf("got err: %v", err) + } +} diff --git a/internal/compare/layer.go b/internal/compare/layer.go new file mode 100644 index 0000000..01e63aa --- /dev/null +++ b/internal/compare/layer.go @@ -0,0 +1,80 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "errors" + "fmt" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Layers compares the given layers to each other and returns an error if they +// differ. Note that this does not compare the actual contents (by calling +// Compressed or Uncompressed). +func Layers(a, b v1.Layer) error { + digests := []v1.Hash{} + diffids := []v1.Hash{} + sizes := []int64{} + mts := []types.MediaType{} + errs := []string{} + + for _, layer := range []v1.Layer{a, b} { + digest, err := layer.Digest() + if err != nil { + return err + } + digests = append(digests, digest) + + diffid, err := layer.DiffID() + if err != nil { + return err + } + diffids = append(diffids, diffid) + + size, err := layer.Size() + if err != nil { + return err + } + sizes = append(sizes, size) + + mt, err := layer.MediaType() + if err != nil { + return err + } + mts = append(mts, mt) + } + + if want, got := digests[0], digests[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Digest() != b.Digest(); %s != %s", want, got)) + } + if want, got := diffids[0], diffids[1]; want != got { + errs = append(errs, fmt.Sprintf("a.DiffID() != b.DiffID(); %s != %s", want, got)) + } + if want, got := sizes[0], sizes[1]; want != got { + errs = append(errs, fmt.Sprintf("a.Size() != b.Size(); %d != %d", want, got)) + } + if want, got := mts[0], mts[1]; want != got { + errs = append(errs, fmt.Sprintf("a.MediaType() != b.MediaType(); %s != %s", want, got)) + } + + if len(errs) != 0 { + return errors.New("Layers differ:\n" + strings.Join(errs, "\n")) + } + + return nil +} diff --git a/internal/compare/layer_test.go b/internal/compare/layer_test.go new file mode 100644 index 0000000..6db9751 --- /dev/null +++ b/internal/compare/layer_test.go @@ -0,0 +1,48 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compare + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestDifferentLayers(t *testing.T) { + a, err := random.Layer(100, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + b, err := random.Layer(100, types.OCILayer) + if err != nil { + t.Fatal(err) + } + + if err := Layers(a, b); err == nil { + t.Errorf("got nil err, should have something") + } +} + +func TestEqualLayers(t *testing.T) { + a, err := random.Layer(100, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + + if err := Layers(a, a); err != nil { + t.Errorf("got err: %v", err) + } +} diff --git a/internal/compression/compression.go b/internal/compression/compression.go new file mode 100644 index 0000000..0124871 --- /dev/null +++ b/internal/compression/compression.go @@ -0,0 +1,97 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package compression abstracts over gzip and zstd. +package compression + +import ( + "bufio" + "bytes" + "io" + + "github.com/google/go-containerregistry/internal/gzip" + "github.com/google/go-containerregistry/internal/zstd" + "github.com/google/go-containerregistry/pkg/compression" +) + +// Opener represents e.g. opening a file. +type Opener = func() (io.ReadCloser, error) + +// GetCompression detects whether an Opener is compressed and which algorithm is used. +func GetCompression(opener Opener) (compression.Compression, error) { + rc, err := opener() + if err != nil { + return compression.None, err + } + defer rc.Close() + + cp, _, err := PeekCompression(rc) + if err != nil { + return compression.None, err + } + + return cp, nil +} + +// PeekCompression detects whether the input stream is compressed and which algorithm is used. +// +// If r implements Peek, we will use that directly, otherwise a small number +// of bytes are buffered to Peek at the gzip/zstd header, and the returned +// PeekReader can be used as a replacement for the consumed input io.Reader. +func PeekCompression(r io.Reader) (compression.Compression, PeekReader, error) { + pr := intoPeekReader(r) + + if isGZip, _, err := checkHeader(pr, gzip.MagicHeader); err != nil { + return compression.None, pr, err + } else if isGZip { + return compression.GZip, pr, nil + } + + if isZStd, _, err := checkHeader(pr, zstd.MagicHeader); err != nil { + return compression.None, pr, err + } else if isZStd { + return compression.ZStd, pr, nil + } + + return compression.None, pr, nil +} + +// PeekReader is an io.Reader that also implements Peek a la bufio.Reader. +type PeekReader interface { + io.Reader + Peek(n int) ([]byte, error) +} + +// IntoPeekReader creates a PeekReader from an io.Reader. +// If the reader already has a Peek method, it will just return the passed reader. +func intoPeekReader(r io.Reader) PeekReader { + if p, ok := r.(PeekReader); ok { + return p + } + + return bufio.NewReader(r) +} + +// CheckHeader checks whether the first bytes from a PeekReader match an expected header +func checkHeader(pr PeekReader, expectedHeader []byte) (bool, PeekReader, error) { + header, err := pr.Peek(len(expectedHeader)) + if err != nil { + // https://github.com/google/go-containerregistry/issues/367 + if err == io.EOF { + return false, pr, nil + } + return false, pr, err + } + return bytes.Equal(header, expectedHeader), pr, nil +} diff --git a/internal/compression/compression_test.go b/internal/compression/compression_test.go new file mode 100644 index 0000000..5279dfe --- /dev/null +++ b/internal/compression/compression_test.go @@ -0,0 +1,78 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package compression + +import ( + "bytes" + "io" + "testing" + + "github.com/google/go-containerregistry/internal/and" + "github.com/google/go-containerregistry/internal/gzip" + "github.com/google/go-containerregistry/internal/zstd" +) + +type Compressor = func(rc io.ReadCloser) io.ReadCloser +type Decompressor = func(rc io.ReadCloser) (io.ReadCloser, error) + +func testPeekCompression(t *testing.T, + compressionExpected string, + compress Compressor, + decompress Decompressor, +) { + content := "This is the input string." + contentBuf := bytes.NewBufferString(content) + + compressed := compress(io.NopCloser(contentBuf)) + compressionDetected, pr, err := PeekCompression(compressed) + if err != nil { + t.Error("PeekCompression() =", err) + } + + if got := string(compressionDetected); got != compressionExpected { + t.Errorf("PeekCompression(); got %q, content %q", got, compressionExpected) + } + + decompressed, err := decompress(withCloser(pr, compressed)) + if err != nil { + t.Fatal(err) + } + + b, err := io.ReadAll(decompressed) + if err != nil { + t.Error("ReadAll() =", err) + } + + if got := string(b); got != content { + t.Errorf("ReadAll(); got %q, content %q", got, content) + } +} + +func TestPeekCompression(t *testing.T) { + testPeekCompression(t, "gzip", gzip.ReadCloser, gzip.UnzipReadCloser) + testPeekCompression(t, "zstd", zstd.ReadCloser, zstd.UnzipReadCloser) + + nopCompress := func(rc io.ReadCloser) io.ReadCloser { return rc } + nopDecompress := func(rc io.ReadCloser) (io.ReadCloser, error) { return rc, nil } + + testPeekCompression(t, "none", nopCompress, nopDecompress) +} + +func withCloser(pr PeekReader, rc io.ReadCloser) io.ReadCloser { + return &and.ReadCloser{ + Reader: pr, + CloseFunc: rc.Close, + } +} diff --git a/internal/depcheck/depcheck.go b/internal/depcheck/depcheck.go new file mode 100644 index 0000000..ba24665 --- /dev/null +++ b/internal/depcheck/depcheck.go @@ -0,0 +1,186 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package depcheck defines a test utility for ensuring certain packages don't +// take on heavy dependencies. +// +// This is forked from https://pkg.go.dev/knative.dev/pkg/depcheck +package depcheck + +import ( + "fmt" + "sort" + "strings" + "testing" + + "golang.org/x/tools/go/packages" +) + +type node struct { + importpath string + consumers map[string]struct{} +} + +type graph map[string]node + +func (g graph) contains(name string) bool { + _, ok := g[name] + return ok +} + +func (g graph) order() []string { + order := make(sort.StringSlice, 0, len(g)) + for k := range g { + order = append(order, k) + } + order.Sort() + return order +} + +// path constructs an examplary path that looks something like: +// +// knative.dev/pkg/apis/duck +// knative.dev/pkg/apis # Also: [knative.dev/pkg/kmeta knative.dev/pkg/tracker] +// k8s.io/api/core/v1 +func (g graph) path(name string) []string { + n := g[name] + // Base case. + if len(n.consumers) == 0 { + return []string{name} + } + // Inductive step. + consumers := make(sort.StringSlice, 0, len(n.consumers)) + for k := range n.consumers { + consumers = append(consumers, k) + } + consumers.Sort() + base := g.path(consumers[0]) + if len(base) > 1 { // Don't decorate the first entry, which is always an entrypoint. + if len(consumers) > 1 { + // Attach other consumers to the last entry in base. + base = append(base[:len(base)-1], fmt.Sprintf("%s # Also: %v", consumers[0], consumers[1:])) + } + } + return append(base, name) +} + +func buildGraph(importpath string, buildFlags ...string) (graph, error) { + g := make(graph, 1) + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedModule, + BuildFlags: buildFlags, + }, importpath) + if err != nil { + return nil, err + } + packages.Visit(pkgs, func(pkg *packages.Package) bool { + g[pkg.PkgPath] = node{ + importpath: pkg.PkgPath, + consumers: make(map[string]struct{}), + } + return pkg.Module != nil + }, func(pkg *packages.Package) { + for _, imp := range pkg.Imports { + if _, ok := g[imp.PkgPath]; ok { + g[imp.PkgPath].consumers[pkg.PkgPath] = struct{}{} + } + } + }) + return g, nil +} + +// StdlibPackages returns the list of all standard library packages, including +// some golang.org/x/ dependencies. +func StdlibPackages() []string { + // pkg/registry is allowed to depend on any stdlib package, so collect + // all of those -- this also includes golang.org/x/ packages. + pkgs, err := packages.Load(nil, "std") + if err != nil { + panic(fmt.Sprintf("Loading stdlib packages: %v", err)) + } + pkgnames := make([]string, len(pkgs)) + for idx, p := range pkgs { + pkgnames[idx] = p.PkgPath + } + return pkgnames +} + +// CheckNoDependency checks that the given import paths (ip) does not +// depend (transitively) on certain banned imports. +func CheckNoDependency(ip string, banned []string, buildFlags ...string) error { + g, err := buildGraph(ip, buildFlags...) + if err != nil { + return fmt.Errorf("buildGraph(%q) = %w", ip, err) + } + for _, dip := range banned { + if g.contains(dip) { + return fmt.Errorf("%s depends on banned dependency %s\n%s", ip, dip, + strings.Join(g.path(dip), "\n")) + } + } + return nil +} + +// AssertNoDependency checks that the given import paths (the keys) do not +// depend (transitively) on certain banned imports (the values) +func AssertNoDependency(t *testing.T, banned map[string][]string, buildFlags ...string) { + t.Helper() + for ip, banned := range banned { + t.Run(ip, func(t *testing.T) { + if err := CheckNoDependency(ip, banned, buildFlags...); err != nil { + t.Error("CheckNoDependency() =", err) + } + }) + } +} + +// AssertOnlyDependencies checks that the given import paths (the keys) only +// depend (transitively) on certain allowed imports (the values). +// Note: while perhaps counterintuitive we allow the value to be a superset +// of the actual imports to that folks can use a constant that holds blessed +// import paths. +func AssertOnlyDependencies(t *testing.T, allowed map[string][]string, buildFlags ...string) { + t.Helper() + for ip, allow := range allowed { + // Always include our own package in the set of allowed dependencies. + allowed := make(map[string]struct{}, len(allow)+1) + for _, x := range append(allow, ip) { + allowed[x] = struct{}{} + } + t.Run(ip, func(t *testing.T) { + if err := CheckOnlyDependencies(ip, allowed, buildFlags...); err != nil { + t.Error("CheckOnlyDependencies() =", err) + } + }) + } +} + +// CheckOnlyDependencies checks that the given import path only +// depends (transitively) on certain allowed imports. +// Note: while perhaps counterintuitive we allow the value to be a superset +// of the actual imports to that folks can use a constant that holds blessed +// import paths. +func CheckOnlyDependencies(ip string, allowed map[string]struct{}, buildFlags ...string) error { + g, err := buildGraph(ip, buildFlags...) + if err != nil { + return fmt.Errorf("buildGraph(%q) = %w", ip, err) + } + for _, name := range g.order() { + if _, ok := allowed[name]; !ok { + return fmt.Errorf("dependency %s of %s is not explicitly allowed\n%s", name, ip, + strings.Join(g.path(name), "\n")) + } + } + return nil +} diff --git a/internal/editor/editor.go b/internal/editor/editor.go new file mode 100644 index 0000000..6a70fa0 --- /dev/null +++ b/internal/editor/editor.go @@ -0,0 +1,64 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package editor implements a simple interface for interactive file editing. +// It most likely does not work on windows. +package editor + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" +) + +// Edit opens a temporary file in the default editor (per $EDITOR, falling back +// to "vi") with the contents of the given io.Reader and a filename ending in +// the given extension (to give a hint to the editor for syntax highlighting). +// +// The contents of the edited file are returned, and the temporary file removed. +func Edit(input io.Reader, extension string) ([]byte, error) { + f, err := os.CreateTemp("", fmt.Sprintf("%s-edit.*.%s", filepath.Base(os.Args[0]), extension)) + if err != nil { + return nil, err + } + defer os.Remove(f.Name()) + + if _, err := io.Copy(f, input); err != nil { + return nil, err + } + f.Close() + + editor := "vi" + if env := os.Getenv("EDITOR"); env != "" { + editor = env + } + + path, err := exec.LookPath(editor) + if err != nil { + return nil, err + } + + cmd := exec.Command(path, f.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return nil, err + } + + return os.ReadFile(f.Name()) +} diff --git a/internal/estargz/estargz.go b/internal/estargz/estargz.go new file mode 100644 index 0000000..69021bc --- /dev/null +++ b/internal/estargz/estargz.go @@ -0,0 +1,54 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package estargz adapts the containerd estargz package to our abstractions. +package estargz + +import ( + "bytes" + "io" + + "github.com/containerd/stargz-snapshotter/estargz" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// Assert that what we're returning is an io.ReadCloser +var _ io.ReadCloser = (*estargz.Blob)(nil) + +// ReadCloser reads uncompressed tarball input from the io.ReadCloser and +// returns: +// - An io.ReadCloser from which compressed data may be read, and +// - A v1.Hash with the hash of the estargz table of contents, or +// - An error if the estargz processing encountered a problem. +// +// Refer to estargz for the options: +// https://pkg.go.dev/github.com/containerd/stargz-snapshotter/estargz@v0.4.1#Option +func ReadCloser(r io.ReadCloser, opts ...estargz.Option) (*estargz.Blob, v1.Hash, error) { + defer r.Close() + + // TODO(#876): Avoid buffering into memory. + bs, err := io.ReadAll(r) + if err != nil { + return nil, v1.Hash{}, err + } + br := bytes.NewReader(bs) + + rc, err := estargz.Build(io.NewSectionReader(br, 0, int64(len(bs))), opts...) + if err != nil { + return nil, v1.Hash{}, err + } + + h, err := v1.NewHash(rc.TOCDigest().String()) + return rc, h, err +} diff --git a/internal/estargz/estargz_test.go b/internal/estargz/estargz_test.go new file mode 100644 index 0000000..1eb3114 --- /dev/null +++ b/internal/estargz/estargz_test.go @@ -0,0 +1,108 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package estargz + +import ( + "archive/tar" + "bytes" + "fmt" + "io" + "strings" + "testing" + + "github.com/google/go-containerregistry/internal/gzip" +) + +func TestReader(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBuffer(nil) + tw := tar.NewWriter(buf) + + if err := tw.WriteHeader(&tar.Header{ + Name: "foo", + Size: int64(len(want)), + }); err != nil { + t.Fatal("WriteHeader() =", err) + } + if _, err := tw.Write([]byte(want)); err != nil { + t.Fatal("tw.Write() =", err) + } + tw.Close() + + zipped, _, err := ReadCloser(io.NopCloser(buf)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + unzipped, err := gzip.UnzipReadCloser(zipped) + if err != nil { + t.Error("gzip.UnzipReadCloser() =", err) + } + defer unzipped.Close() + + found := false + + r := tar.NewReader(unzipped) + for { + hdr, err := r.Next() + if err == io.EOF { + break + } else if err != nil { + t.Fatal("tar.Next() =", err) + } + + if hdr.Name != "foo" { + continue + } + found = true + + b, err := io.ReadAll(r) + if err != nil { + t.Error("ReadAll() =", err) + } + if got := string(b); got != want { + t.Errorf("ReadAll(); got %q, want %q", got, want) + } + if err := unzipped.Close(); err != nil { + t.Error("Close() =", err) + } + } + + if !found { + t.Error(`Did not find the expected file "foo"`) + } +} + +var ( + errRead = fmt.Errorf("Read failed") +) + +type failReader struct{} + +func (f failReader) Read(_ []byte) (int, error) { + return 0, errRead +} + +func TestReadErrors(t *testing.T) { + fr := failReader{} + + if _, _, err := ReadCloser(io.NopCloser(fr)); err != errRead { + t.Error("ReadCloser: expected errRead, got", err) + } + + buf := bytes.NewBufferString("not a tarball") + if _, _, err := ReadCloser(io.NopCloser(buf)); !strings.Contains(err.Error(), "failed to parse tar file") { + t.Error(`ReadCloser: expected "failed to parse tar file", got`, err) + } +} diff --git a/internal/gzip/zip.go b/internal/gzip/zip.go new file mode 100644 index 0000000..018c0f8 --- /dev/null +++ b/internal/gzip/zip.go @@ -0,0 +1,118 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gzip provides helper functions for interacting with gzipped streams. +package gzip + +import ( + "bufio" + "bytes" + "compress/gzip" + "io" + + "github.com/google/go-containerregistry/internal/and" +) + +// MagicHeader is the start of gzip files. +var MagicHeader = []byte{'\x1f', '\x8b'} + +// ReadCloser reads uncompressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which compressed data may be read. +// This uses gzip.BestSpeed for the compression level. +func ReadCloser(r io.ReadCloser) io.ReadCloser { + return ReadCloserLevel(r, gzip.BestSpeed) +} + +// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which compressed data may be read. +// Refer to compress/gzip for the level: +// https://golang.org/pkg/compress/gzip/#pkg-constants +func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser { + pr, pw := io.Pipe() + + // For highly compressible layers, gzip.Writer will output a very small + // number of bytes per Write(). This is normally fine, but when pushing + // to a registry, we want to ensure that we're taking full advantage of + // the available bandwidth instead of sending tons of tiny writes over + // the wire. + // 64K ought to be small enough for anybody. + bw := bufio.NewWriterSize(pw, 2<<16) + + // Returns err so we can pw.CloseWithError(err) + go func() error { + // TODO(go1.14): Just defer {pw,gw,r}.Close like you'd expect. + // Context: https://golang.org/issue/24283 + gw, err := gzip.NewWriterLevel(bw, level) + if err != nil { + return pw.CloseWithError(err) + } + + if _, err := io.Copy(gw, r); err != nil { + defer r.Close() + defer gw.Close() + return pw.CloseWithError(err) + } + + // Close gzip writer to Flush it and write gzip trailers. + if err := gw.Close(); err != nil { + return pw.CloseWithError(err) + } + + // Flush bufio writer to ensure we write out everything. + if err := bw.Flush(); err != nil { + return pw.CloseWithError(err) + } + + // We don't really care if these fail. + defer pw.Close() + defer r.Close() + + return nil + }() + + return pr +} + +// UnzipReadCloser reads compressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which uncompressed data may be read. +func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) { + gr, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + return &and.ReadCloser{ + Reader: gr, + CloseFunc: func() error { + // If the unzip fails, then this seems to return the same + // error as the read. We don't want this to interfere with + // us closing the main ReadCloser, since this could leave + // an open file descriptor (fails on Windows). + gr.Close() + return r.Close() + }, + }, nil +} + +// Is detects whether the input stream is compressed. +func Is(r io.Reader) (bool, error) { + magicHeader := make([]byte, 2) + n, err := r.Read(magicHeader) + if n == 0 && err == io.EOF { + return false, nil + } + if err != nil { + return false, err + } + return bytes.Equal(magicHeader, MagicHeader), nil +} diff --git a/internal/gzip/zip_test.go b/internal/gzip/zip_test.go new file mode 100644 index 0000000..d8c27f6 --- /dev/null +++ b/internal/gzip/zip_test.go @@ -0,0 +1,98 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gzip + +import ( + "bytes" + "fmt" + "io" + "strings" + "testing" +) + +func TestReader(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + zipped := ReadCloser(io.NopCloser(buf)) + unzipped, err := UnzipReadCloser(zipped) + if err != nil { + t.Error("UnzipReadCloser() =", err) + } + + b, err := io.ReadAll(unzipped) + if err != nil { + t.Error("ReadAll() =", err) + } + if got := string(b); got != want { + t.Errorf("ReadAll(); got %q, want %q", got, want) + } + if err := unzipped.Close(); err != nil { + t.Error("Close() =", err) + } +} + +func TestIs(t *testing.T) { + tests := []struct { + in []byte + out bool + err error + }{ + {[]byte{}, false, nil}, + {[]byte{'\x00', '\x00', '\x00'}, false, nil}, + {[]byte{'\x1f', '\x8b', '\x1b'}, true, nil}, + } + for _, test := range tests { + reader := bytes.NewReader(test.in) + got, err := Is(reader) + if got != test.out { + t.Errorf("Is; n: got %v, wanted %v\n", got, test.out) + } + if err != test.err { + t.Errorf("Is; err: got %v, wanted %v\n", err, test.err) + } + } +} + +var ( + errRead = fmt.Errorf("Read failed") +) + +type failReader struct{} + +func (f failReader) Read(_ []byte) (int, error) { + return 0, errRead +} + +func TestReadErrors(t *testing.T) { + fr := failReader{} + if _, err := Is(fr); err != errRead { + t.Error("Is: expected errRead, got", err) + } + + frc := io.NopCloser(fr) + if _, err := UnzipReadCloser(frc); err != errRead { + t.Error("UnzipReadCloser: expected errRead, got", err) + } + + zr := ReadCloser(io.NopCloser(fr)) + if _, err := zr.Read(nil); err != errRead { + t.Error("ReadCloser: expected errRead, got", err) + } + + zr = ReadCloserLevel(io.NopCloser(strings.NewReader("zip me")), -10) + if _, err := zr.Read(nil); err == nil { + t.Error("Expected invalid level error, got:", err) + } +} diff --git a/internal/httptest/httptest.go b/internal/httptest/httptest.go new file mode 100644 index 0000000..85b1719 --- /dev/null +++ b/internal/httptest/httptest.go @@ -0,0 +1,104 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package httptest provides a method for testing a TLS server a la net/http/httptest. +package httptest + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + "net" + "net/http" + "net/http/httptest" + "time" +) + +// NewTLSServer returns an httptest server, with an http client that has been configured to +// send all requests to the returned server. The TLS certs are generated for the given domain. +// If you need a transport, Client().Transport is correctly configured. +func NewTLSServer(domain string, handler http.Handler) (*httptest.Server, error) { + s := httptest.NewUnstartedServer(handler) + + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(time.Hour), + IPAddresses: []net.IP{ + net.IPv4(127, 0, 0, 1), + net.IPv6loopback, + }, + DNSNames: []string{domain}, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, err + } + + b, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, err + } + + pc := &bytes.Buffer{} + if err := pem.Encode(pc, &pem.Block{Type: "CERTIFICATE", Bytes: b}); err != nil { + return nil, err + } + + ek, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return nil, err + } + + pk := &bytes.Buffer{} + if err := pem.Encode(pk, &pem.Block{Type: "EC PRIVATE KEY", Bytes: ek}); err != nil { + return nil, err + } + + c, err := tls.X509KeyPair(pc.Bytes(), pk.Bytes()) + if err != nil { + return nil, err + } + s.TLS = &tls.Config{ + Certificates: []tls.Certificate{c}, + } + s.StartTLS() + + certpool := x509.NewCertPool() + certpool.AddCert(s.Certificate()) + + t := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certpool, + }, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial(s.Listener.Addr().Network(), s.Listener.Addr().String()) + }, + } + s.Client().Transport = t + + return s, nil +} diff --git a/internal/legacy/copy.go b/internal/legacy/copy.go new file mode 100644 index 0000000..10467ba --- /dev/null +++ b/internal/legacy/copy.go @@ -0,0 +1,57 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package legacy provides methods for interacting with legacy image formats. +package legacy + +import ( + "bytes" + "encoding/json" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// CopySchema1 allows `[g]crane cp` to work with old images without adding +// full support for schema 1 images to this package. +func CopySchema1(desc *remote.Descriptor, srcRef, dstRef name.Reference, opts ...remote.Option) error { + m := schema1{} + if err := json.NewDecoder(bytes.NewReader(desc.Manifest)).Decode(&m); err != nil { + return err + } + + for _, layer := range m.FSLayers { + src := srcRef.Context().Digest(layer.BlobSum) + dst := dstRef.Context().Digest(layer.BlobSum) + + blob, err := remote.Layer(src, opts...) + if err != nil { + return err + } + + if err := remote.WriteLayer(dst.Context(), blob, opts...); err != nil { + return err + } + } + + return remote.Put(dstRef, desc, opts...) +} + +type fslayer struct { + BlobSum string `json:"blobSum"` +} + +type schema1 struct { + FSLayers []fslayer `json:"fsLayers"` +} diff --git a/internal/legacy/copy_test.go b/internal/legacy/copy_test.go new file mode 100644 index 0000000..b8ca799 --- /dev/null +++ b/internal/legacy/copy_test.go @@ -0,0 +1,97 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy + +import ( + "encoding/json" + "fmt" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestCopySchema1(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + // We'll copy from src to dst. + src := fmt.Sprintf("%s/schema1/src", u.Host) + srcRef, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/schema1/dst", u.Host) + dstRef, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + // Create a random layer. + layer, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + digest, err := layer.Digest() + if err != nil { + t.Fatal(err) + } + layerRef, err := name.NewDigest(fmt.Sprintf("%s@%s", src, digest)) + if err != nil { + t.Fatal(err) + } + + // Populate the registry with a layer and a schema 1 manifest referencing it. + if err := remote.WriteLayer(layerRef.Context(), layer); err != nil { + t.Fatal(err) + } + manifest := schema1{ + FSLayers: []fslayer{{ + BlobSum: digest.String(), + }}, + } + b, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + desc := &remote.Descriptor{ + Manifest: b, + Descriptor: v1.Descriptor{ + MediaType: types.DockerManifestSchema1, + Digest: v1.Hash{Algorithm: "sha256", + Hex: strings.Repeat("a", 64), + }, + }, + } + if err := remote.Put(dstRef, desc); err != nil { + t.Fatal(err) + } + + if err := CopySchema1(desc, srcRef, dstRef); err != nil { + t.Errorf("failed to copy schema 1: %v", err) + } +} diff --git a/internal/redact/redact.go b/internal/redact/redact.go new file mode 100644 index 0000000..b2e3f18 --- /dev/null +++ b/internal/redact/redact.go @@ -0,0 +1,89 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package redact contains a simple context signal for redacting requests. +package redact + +import ( + "context" + "errors" + "net/url" +) + +type contextKey string + +var redactKey = contextKey("redact") + +// NewContext creates a new ctx with the reason for redaction. +func NewContext(ctx context.Context, reason string) context.Context { + return context.WithValue(ctx, redactKey, reason) +} + +// FromContext returns the redaction reason, if any. +func FromContext(ctx context.Context) (bool, string) { + reason, ok := ctx.Value(redactKey).(string) + return ok, reason +} + +// Error redacts potentially sensitive query parameter values in the URL from the error's message. +// +// If the error is a *url.Error, this returns a *url.Error with the URL redacted. +// Any other error type, or nil, is returned unchanged. +func Error(err error) error { + // If the error is a url.Error, we can redact the URL. + // Otherwise (including if err is nil), we can't redact. + var uerr *url.Error + if ok := errors.As(err, &uerr); !ok { + return err + } + u, perr := url.Parse(uerr.URL) + if perr != nil { + return err // If the URL can't be parsed, just return the original error. + } + uerr.URL = URL(u).String() // Update the URL to the redacted URL. + return uerr +} + +// The set of query string keys that we expect to send as part of the registry +// protocol. Anything else is potentially dangerous to leak, as it's probably +// from a redirect. These redirects often included tokens or signed URLs. +var paramAllowlist = map[string]struct{}{ + // Token exchange + "scope": {}, + "service": {}, + // Cross-repo mounting + "mount": {}, + "from": {}, + // Layer PUT + "digest": {}, + // Listing tags and catalog + "n": {}, + "last": {}, +} + +// URL redacts potentially sensitive query parameter values from the URL's query string. +func URL(u *url.URL) *url.URL { + qs := u.Query() + for k, v := range qs { + for i := range v { + if _, ok := paramAllowlist[k]; !ok { + // key is not in the Allowlist + v[i] = "REDACTED" + } + } + } + r := *u + r.RawQuery = qs.Encode() + return &r +} diff --git a/internal/retry/retry.go b/internal/retry/retry.go new file mode 100644 index 0000000..c9e3564 --- /dev/null +++ b/internal/retry/retry.go @@ -0,0 +1,94 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package retry provides methods for retrying operations. It is a thin wrapper +// around k8s.io/apimachinery/pkg/util/wait to make certain operations easier. +package retry + +import ( + "context" + "errors" + "fmt" + + "github.com/google/go-containerregistry/internal/retry/wait" +) + +// Backoff is an alias of our own wait.Backoff to avoid name conflicts with +// the kubernetes wait package. Typing retry.Backoff is aesier than fixing +// the wrong import every time you use wait.Backoff. +type Backoff = wait.Backoff + +// This is implemented by several errors in the net package as well as our +// transport.Error. +type temporary interface { + Temporary() bool +} + +// IsTemporary returns true if err implements Temporary() and it returns true. +func IsTemporary(err error) bool { + if errors.Is(err, context.DeadlineExceeded) { + return false + } + if te, ok := err.(temporary); ok && te.Temporary() { + return true + } + return false +} + +// IsNotNil returns true if err is not nil. +func IsNotNil(err error) bool { + return err != nil +} + +// Predicate determines whether an error should be retried. +type Predicate func(error) (retry bool) + +// Retry retries a given function, f, until a predicate is satisfied, using +// exponential backoff. If the predicate is never satisfied, it will return the +// last error returned by f. +func Retry(f func() error, p Predicate, backoff wait.Backoff) (err error) { + if f == nil { + return fmt.Errorf("nil f passed to retry") + } + if p == nil { + return fmt.Errorf("nil p passed to retry") + } + + condition := func() (bool, error) { + err = f() + if p(err) { + return false, nil + } + return true, err + } + + wait.ExponentialBackoff(backoff, condition) + return +} + +type contextKey string + +var key = contextKey("never") + +// Never returns a context that signals something should not be retried. +// This is a hack and can be used to communicate across package boundaries +// to avoid retry amplification. +func Never(ctx context.Context) context.Context { + return context.WithValue(ctx, key, true) +} + +// Ever returns true if the context was wrapped by Never. +func Ever(ctx context.Context) bool { + return ctx.Value(key) == nil +} diff --git a/internal/retry/retry_test.go b/internal/retry/retry_test.go new file mode 100644 index 0000000..2091ca5 --- /dev/null +++ b/internal/retry/retry_test.go @@ -0,0 +1,100 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package retry + +import ( + "context" + "fmt" + "net/http" + "net/url" + "testing" +) + +type temp struct{} + +func (e temp) Error() string { + return "temporary error" +} + +func (e temp) Temporary() bool { + return true +} + +func TestRetry(t *testing.T) { + for i, test := range []struct { + predicate Predicate + err error + shouldRetry bool + }{{ + predicate: IsTemporary, + err: nil, + shouldRetry: false, + }, { + predicate: IsTemporary, + err: fmt.Errorf("not temporary"), + shouldRetry: false, + }, { + predicate: IsNotNil, + err: fmt.Errorf("not temporary"), + shouldRetry: true, + }, { + predicate: IsTemporary, + err: temp{}, + shouldRetry: true, + }, { + predicate: IsTemporary, + err: context.DeadlineExceeded, + shouldRetry: false, + }, { + predicate: IsTemporary, + err: &url.Error{ + Op: http.MethodPost, + URL: "http://127.0.0.1:56520/v2/example/blobs/uploads/", + Err: context.DeadlineExceeded, + }, + shouldRetry: false, + }} { + // Make sure we retry 5 times if we shouldRetry. + steps := 5 + backoff := Backoff{ + Steps: steps, + } + + // Count how many times this function is invoked. + count := 0 + f := func() error { + count++ + return test.err + } + + Retry(f, test.predicate, backoff) + + if test.shouldRetry && count != steps { + t.Errorf("expected %d to retry %v, did not", i, test.err) + } else if !test.shouldRetry && count == steps { + t.Errorf("expected %d not to retry %v, but did", i, test.err) + } + } +} + +// Make sure we don't panic. +func TestNil(t *testing.T) { + if err := Retry(nil, nil, Backoff{}); err == nil { + t.Errorf("got nil when passing in nil f") + } + if err := Retry(func() error { return nil }, nil, Backoff{}); err == nil { + t.Errorf("got nil when passing in nil p") + } +} diff --git a/internal/retry/wait/kubernetes_apimachinery_wait.go b/internal/retry/wait/kubernetes_apimachinery_wait.go new file mode 100644 index 0000000..ab06e5f --- /dev/null +++ b/internal/retry/wait/kubernetes_apimachinery_wait.go @@ -0,0 +1,123 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package wait is a subset of k8s.io/apimachinery to avoid conflicts +// in dependencies (specifically, logging). +package wait + +import ( + "errors" + "math/rand" + "time" +) + +// Jitter returns a time.Duration between duration and duration + maxFactor * +// duration. +// +// This allows clients to avoid converging on periodic behavior. If maxFactor +// is 0.0, a suggested default value will be chosen. +func Jitter(duration time.Duration, maxFactor float64) time.Duration { + if maxFactor <= 0.0 { + maxFactor = 1.0 + } + wait := duration + time.Duration(rand.Float64()*maxFactor*float64(duration)) + return wait +} + +// ErrWaitTimeout is returned when the condition exited without success. +var ErrWaitTimeout = errors.New("timed out waiting for the condition") + +// ConditionFunc returns true if the condition is satisfied, or an error +// if the loop should be aborted. +type ConditionFunc func() (done bool, err error) + +// Backoff holds parameters applied to a Backoff function. +type Backoff struct { + // The initial duration. + Duration time.Duration + // Duration is multiplied by factor each iteration, if factor is not zero + // and the limits imposed by Steps and Cap have not been reached. + // Should not be negative. + // The jitter does not contribute to the updates to the duration parameter. + Factor float64 + // The sleep at each iteration is the duration plus an additional + // amount chosen uniformly at random from the interval between + // zero and `jitter*duration`. + Jitter float64 + // The remaining number of iterations in which the duration + // parameter may change (but progress can be stopped earlier by + // hitting the cap). If not positive, the duration is not + // changed. Used for exponential backoff in combination with + // Factor and Cap. + Steps int + // A limit on revised values of the duration parameter. If a + // multiplication by the factor parameter would make the duration + // exceed the cap then the duration is set to the cap and the + // steps parameter is set to zero. + Cap time.Duration +} + +// Step (1) returns an amount of time to sleep determined by the +// original Duration and Jitter and (2) mutates the provided Backoff +// to update its Steps and Duration. +func (b *Backoff) Step() time.Duration { + if b.Steps < 1 { + if b.Jitter > 0 { + return Jitter(b.Duration, b.Jitter) + } + return b.Duration + } + b.Steps-- + + duration := b.Duration + + // calculate the next step + if b.Factor != 0 { + b.Duration = time.Duration(float64(b.Duration) * b.Factor) + if b.Cap > 0 && b.Duration > b.Cap { + b.Duration = b.Cap + b.Steps = 0 + } + } + + if b.Jitter > 0 { + duration = Jitter(duration, b.Jitter) + } + return duration +} + +// ExponentialBackoff repeats a condition check with exponential backoff. +// +// It repeatedly checks the condition and then sleeps, using `backoff.Step()` +// to determine the length of the sleep and adjust Duration and Steps. +// Stops and returns as soon as: +// 1. the condition check returns true or an error, +// 2. `backoff.Steps` checks of the condition have been done, or +// 3. a sleep truncated by the cap on duration has been completed. +// In case (1) the returned error is what the condition function returned. +// In all other cases, ErrWaitTimeout is returned. +func ExponentialBackoff(backoff Backoff, condition ConditionFunc) error { + for backoff.Steps > 0 { + if ok, err := condition(); err != nil || ok { + return err + } + if backoff.Steps == 1 { + break + } + time.Sleep(backoff.Step()) + } + return ErrWaitTimeout +} diff --git a/internal/verify/verify.go b/internal/verify/verify.go new file mode 100644 index 0000000..463f7e4 --- /dev/null +++ b/internal/verify/verify.go @@ -0,0 +1,122 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package verify provides a ReadCloser that verifies content matches the +// expected hash values. +package verify + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "hash" + "io" + + "github.com/google/go-containerregistry/internal/and" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// SizeUnknown is a sentinel value to indicate that the expected size is not known. +const SizeUnknown = -1 + +type verifyReader struct { + inner io.Reader + hasher hash.Hash + expected v1.Hash + gotSize, wantSize int64 +} + +// Error provides information about the failed hash verification. +type Error struct { + got string + want v1.Hash + gotSize int64 +} + +func (v Error) Error() string { + return fmt.Sprintf("error verifying %s checksum after reading %d bytes; got %q, want %q", + v.want.Algorithm, v.gotSize, v.got, v.want) +} + +// Read implements io.Reader +func (vc *verifyReader) Read(b []byte) (int, error) { + n, err := vc.inner.Read(b) + vc.gotSize += int64(n) + if err == io.EOF { + if vc.wantSize != SizeUnknown && vc.gotSize != vc.wantSize { + return n, fmt.Errorf("error verifying size; got %d, want %d", vc.gotSize, vc.wantSize) + } + got := hex.EncodeToString(vc.hasher.Sum(nil)) + if want := vc.expected.Hex; got != want { + return n, Error{ + got: vc.expected.Algorithm + ":" + got, + want: vc.expected, + gotSize: vc.gotSize, + } + } + } + return n, err +} + +// ReadCloser wraps the given io.ReadCloser to verify that its contents match +// the provided v1.Hash before io.EOF is returned. +// +// The reader will only be read up to size bytes, to prevent resource +// exhaustion. If EOF is returned before size bytes are read, an error is +// returned. +// +// A size of SizeUnknown (-1) indicates disables size verification when the size +// is unknown ahead of time. +func ReadCloser(r io.ReadCloser, size int64, h v1.Hash) (io.ReadCloser, error) { + w, err := v1.Hasher(h.Algorithm) + if err != nil { + return nil, err + } + r2 := io.TeeReader(r, w) // pass all writes to the hasher. + if size != SizeUnknown { + r2 = io.LimitReader(r2, size) // if we know the size, limit to that size. + } + return &and.ReadCloser{ + Reader: &verifyReader{ + inner: r2, + hasher: w, + expected: h, + wantSize: size, + }, + CloseFunc: r.Close, + }, nil +} + +// Descriptor verifies that the embedded Data field matches the Size and Digest +// fields of the given v1.Descriptor, returning an error if the Data field is +// missing or if it contains incorrect data. +func Descriptor(d v1.Descriptor) error { + if d.Data == nil { + return errors.New("error verifying descriptor; Data == nil") + } + + h, sz, err := v1.SHA256(bytes.NewReader(d.Data)) + if err != nil { + return err + } + if h != d.Digest { + return fmt.Errorf("error verifying Digest; got %q, want %q", h, d.Digest) + } + if sz != d.Size { + return fmt.Errorf("error verifying Size; got %d, want %d", sz, d.Size) + } + + return nil +} diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go new file mode 100644 index 0000000..a2cbbdc --- /dev/null +++ b/internal/verify/verify_test.go @@ -0,0 +1,147 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +func mustHash(s string, t *testing.T) v1.Hash { + h, _, err := v1.SHA256(strings.NewReader(s)) + if err != nil { + t.Fatalf("v1.SHA256(%s) = %v", s, err) + } + t.Logf("Hashed: %q -> %q", s, h) + return h +} + +func TestVerificationFailure(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + + verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash("not the same", t)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + if b, err := io.ReadAll(verified); err == nil { + t.Errorf("ReadAll() = %q; want verification error", string(b)) + } +} + +func TestVerification(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + + verified, err := ReadCloser(io.NopCloser(buf), int64(len(want)), mustHash(want, t)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + if _, err := io.ReadAll(verified); err != nil { + t.Error("ReadAll() =", err) + } +} + +func TestVerificationSizeUnknown(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + + verified, err := ReadCloser(io.NopCloser(buf), SizeUnknown, mustHash(want, t)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + if _, err := io.ReadAll(verified); err != nil { + t.Error("ReadAll() =", err) + } +} + +func TestBadHash(t *testing.T) { + h := v1.Hash{ + Algorithm: "fake256", + Hex: "whatever", + } + _, err := ReadCloser(io.NopCloser(strings.NewReader("hi")), 0, h) + if err == nil { + t.Errorf("ReadCloser() = %v, wanted err", err) + } +} + +func TestBadSize(t *testing.T) { + want := "This is the input string." + + // having too much content or expecting too much content returns an error. + for _, size := range []int64{3, 100} { + t.Run(fmt.Sprintf("expecting size %d", size), func(t *testing.T) { + buf := bytes.NewBufferString(want) + rc, err := ReadCloser(io.NopCloser(buf), size, mustHash(want, t)) + if err != nil { + t.Fatal("ReadCloser() =", err) + } + if b, err := io.ReadAll(rc); err == nil { + t.Errorf("ReadAll() = %q; want verification error", string(b)) + } + }) + } +} + +func TestDescriptor(t *testing.T) { + for _, tc := range []struct { + err error + desc v1.Descriptor + }{{ + err: errors.New("error verifying descriptor; Data == nil"), + }, { + err: errors.New(`error verifying Digest; got "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", want ":"`), + desc: v1.Descriptor{ + Data: []byte("abc"), + }, + }, { + err: errors.New("error verifying Size; got 3, want 0"), + desc: v1.Descriptor{ + Data: []byte("abc"), + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + }, + }, + }, { + desc: v1.Descriptor{ + Data: []byte("abc"), + Size: 3, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + }, + }, + }} { + got, want := Descriptor(tc.desc), tc.err + + if got == nil { + if want != nil { + t.Errorf("Descriptor(): got nil, want %v", want) + } + } else if want == nil { + t.Errorf("Descriptor(): got %v, want nil", got) + } else if got, want := got.Error(), want.Error(); got != want { + t.Errorf("Descriptor(): got %q, want %q", got, want) + } + } +} diff --git a/internal/windows/windows.go b/internal/windows/windows.go new file mode 100644 index 0000000..62d04cf --- /dev/null +++ b/internal/windows/windows.go @@ -0,0 +1,114 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package windows + +import ( + "archive/tar" + "bytes" + "errors" + "fmt" + "io" + "path" + "strings" + + "github.com/google/go-containerregistry/internal/gzip" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// userOwnerAndGroupSID is a magic value needed to make the binary executable +// in a Windows container. +// +// owner: BUILTIN/Users group: BUILTIN/Users ($sddlValue="O:BUG:BU") +const userOwnerAndGroupSID = "AQAAgBQAAAAkAAAAAAAAAAAAAAABAgAAAAAABSAAAAAhAgAAAQIAAAAAAAUgAAAAIQIAAA==" + +// Windows returns a Layer that is converted to be pullable on Windows. +func Windows(layer v1.Layer) (v1.Layer, error) { + // TODO: do this lazily. + + layerReader, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("getting layer: %w", err) + } + defer layerReader.Close() + tarReader := tar.NewReader(layerReader) + w := new(bytes.Buffer) + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + + for _, dir := range []string{"Files", "Hives"} { + if err := tarWriter.WriteHeader(&tar.Header{ + Name: dir, + Typeflag: tar.TypeDir, + // Use a fixed Mode, so that this isn't sensitive to the directory and umask + // under which it was created. Additionally, windows can only set 0222, + // 0444, or 0666, none of which are executable. + Mode: 0555, + Format: tar.FormatPAX, + }); err != nil { + return nil, fmt.Errorf("writing %s directory: %w", dir, err) + } + } + + for { + header, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("reading layer: %w", err) + } + + if strings.HasPrefix(header.Name, "Files/") { + return nil, fmt.Errorf("file path %q already suitable for Windows", header.Name) + } + + header.Name = path.Join("Files", header.Name) + header.Format = tar.FormatPAX + + // TODO: this seems to make the file executable on Windows; + // only do this if the file should be executable. + if header.PAXRecords == nil { + header.PAXRecords = map[string]string{} + } + header.PAXRecords["MSWINDOWS.rawsd"] = userOwnerAndGroupSID + + if err := tarWriter.WriteHeader(header); err != nil { + return nil, fmt.Errorf("writing tar header: %w", err) + } + + if header.Typeflag == tar.TypeReg { + if _, err = io.Copy(tarWriter, tarReader); err != nil { + return nil, fmt.Errorf("writing layer file: %w", err) + } + } + } + + if err := tarWriter.Close(); err != nil { + return nil, err + } + + b := w.Bytes() + // gzip the contents, then create the layer + opener := func() (io.ReadCloser, error) { + return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil + } + layer, err = tarball.LayerFromOpener(opener) + if err != nil { + return nil, fmt.Errorf("creating layer: %w", err) + } + + return layer, nil +} diff --git a/internal/windows/windows_test.go b/internal/windows/windows_test.go new file mode 100644 index 0000000..3caf54c --- /dev/null +++ b/internal/windows/windows_test.go @@ -0,0 +1,81 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package windows + +import ( + "archive/tar" + "errors" + "io" + "reflect" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func TestWindows(t *testing.T) { + tarLayer, err := tarball.LayerFromFile("../../pkg/v1/tarball/testdata/content.tar") + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + win, err := Windows(tarLayer) + if err != nil { + t.Fatalf("Windows: %v", err) + } + if _, err := Windows(win); err == nil { + t.Error("expected an error double-Windowsifying a layer; got nil") + } + + rc, err := win.Uncompressed() + if err != nil { + t.Fatalf("Uncompressed: %v", err) + } + defer rc.Close() + tr := tar.NewReader(rc) + var sawHives, sawFiles bool + for { + h, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if h.Name == "Hives" && h.Typeflag == tar.TypeDir { + sawHives = true + continue + } + if h.Name == "Files" && h.Typeflag == tar.TypeDir { + sawFiles = true + continue + } + if !strings.HasPrefix(h.Name, "Files/") { + t.Errorf("tar entry %q didn't have Files prefix", h.Name) + } + if h.Format != tar.FormatPAX { + t.Errorf("tar entry %q had unexpected Format; got %v, want %v", h.Name, h.Format, tar.FormatPAX) + } + want := map[string]string{ + "MSWINDOWS.rawsd": userOwnerAndGroupSID, + } + if !reflect.DeepEqual(h.PAXRecords, want) { + t.Errorf("tar entry %q: got %v, want %v", h.Name, h.PAXRecords, want) + } + } + if !sawHives { + t.Errorf("didn't see Hives/ directory") + } + if !sawFiles { + t.Errorf("didn't see Files/ directory") + } +} diff --git a/internal/zstd/zstd.go b/internal/zstd/zstd.go new file mode 100644 index 0000000..cccf54a --- /dev/null +++ b/internal/zstd/zstd.go @@ -0,0 +1,116 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package zstd provides helper functions for interacting with zstd streams. +package zstd + +import ( + "bufio" + "bytes" + "io" + + "github.com/google/go-containerregistry/internal/and" + "github.com/klauspost/compress/zstd" +) + +// MagicHeader is the start of zstd files. +var MagicHeader = []byte{'\x28', '\xb5', '\x2f', '\xfd'} + +// ReadCloser reads uncompressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which compressed data may be read. +// This uses zstd level 1 for the compression. +func ReadCloser(r io.ReadCloser) io.ReadCloser { + return ReadCloserLevel(r, 1) +} + +// ReadCloserLevel reads uncompressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which compressed data may be read. +func ReadCloserLevel(r io.ReadCloser, level int) io.ReadCloser { + pr, pw := io.Pipe() + + // For highly compressible layers, zstd.Writer will output a very small + // number of bytes per Write(). This is normally fine, but when pushing + // to a registry, we want to ensure that we're taking full advantage of + // the available bandwidth instead of sending tons of tiny writes over + // the wire. + // 64K ought to be small enough for anybody. + bw := bufio.NewWriterSize(pw, 2<<16) + + // Returns err so we can pw.CloseWithError(err) + go func() error { + // TODO(go1.14): Just defer {pw,zw,r}.Close like you'd expect. + // Context: https://golang.org/issue/24283 + zw, err := zstd.NewWriter(bw, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(level))) + if err != nil { + return pw.CloseWithError(err) + } + + if _, err := io.Copy(zw, r); err != nil { + defer r.Close() + defer zw.Close() + return pw.CloseWithError(err) + } + + // Close zstd writer to Flush it and write zstd trailers. + if err := zw.Close(); err != nil { + return pw.CloseWithError(err) + } + + // Flush bufio writer to ensure we write out everything. + if err := bw.Flush(); err != nil { + return pw.CloseWithError(err) + } + + // We don't really care if these fail. + defer pw.Close() + defer r.Close() + + return nil + }() + + return pr +} + +// UnzipReadCloser reads compressed input data from the io.ReadCloser and +// returns an io.ReadCloser from which uncompressed data may be read. +func UnzipReadCloser(r io.ReadCloser) (io.ReadCloser, error) { + gr, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + return &and.ReadCloser{ + Reader: gr, + CloseFunc: func() error { + // If the unzip fails, then this seems to return the same + // error as the read. We don't want this to interfere with + // us closing the main ReadCloser, since this could leave + // an open file descriptor (fails on Windows). + gr.Close() + return r.Close() + }, + }, nil +} + +// Is detects whether the input stream is compressed. +func Is(r io.Reader) (bool, error) { + magicHeader := make([]byte, 4) + n, err := r.Read(magicHeader) + if n == 0 && err == io.EOF { + return false, nil + } + if err != nil { + return false, err + } + return bytes.Equal(magicHeader, MagicHeader), nil +} diff --git a/internal/zstd/zstd_test.go b/internal/zstd/zstd_test.go new file mode 100644 index 0000000..c422e27 --- /dev/null +++ b/internal/zstd/zstd_test.go @@ -0,0 +1,96 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package zstd + +import ( + "bytes" + "fmt" + "io" + "testing" +) + +func TestReader(t *testing.T) { + want := "This is the input string." + buf := bytes.NewBufferString(want) + zipped := ReadCloser(io.NopCloser(buf)) + unzipped, err := UnzipReadCloser(zipped) + if err != nil { + t.Error("UnzipReadCloser() =", err) + } + + b, err := io.ReadAll(unzipped) + if err != nil { + t.Error("ReadAll() =", err) + } + if got := string(b); got != want { + t.Errorf("ReadAll(); got %q, want %q", got, want) + } + if err := unzipped.Close(); err != nil { + t.Error("Close() =", err) + } +} + +func TestIs(t *testing.T) { + tests := []struct { + in []byte + out bool + err error + }{ + {[]byte{}, false, nil}, + {[]byte{'\x00', '\x00', '\x00', '\x00', '\x00'}, false, nil}, + {[]byte{'\x28', '\xb5', '\x2f', '\xfd', '\x1b'}, true, nil}, + } + for _, test := range tests { + reader := bytes.NewReader(test.in) + got, err := Is(reader) + if got != test.out { + t.Errorf("Is; n: got %v, wanted %v\n", got, test.out) + } + if err != test.err { + t.Errorf("Is; err: got %v, wanted %v\n", err, test.err) + } + } +} + +var ( + errRead = fmt.Errorf("read failed") +) + +type failReader struct{} + +func (f failReader) Read(_ []byte) (int, error) { + return 0, errRead +} + +func TestReadErrors(t *testing.T) { + fr := failReader{} + if _, err := Is(fr); err != errRead { + t.Error("Is: expected errRead, got", err) + } + + frc := io.NopCloser(fr) + if r, err := UnzipReadCloser(frc); err != errRead { + data := make([]byte, 100) + _, err := r.Read(data) + if err != errRead { + t.Error("UnzipReadCloser: expected errRead, got", err) + } + } + + zr := ReadCloser(io.NopCloser(fr)) + if _, err := zr.Read(nil); err != errRead { + t.Error("ReadCloser: expected errRead, got", err) + } +} diff --git a/pkg/authn/README.md b/pkg/authn/README.md new file mode 100644 index 0000000..042bdde --- /dev/null +++ b/pkg/authn/README.md @@ -0,0 +1,322 @@ +# `authn` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/authn?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/authn) + +This README outlines how we acquire and use credentials when interacting with a registry. + +As much as possible, we attempt to emulate `docker`'s authentication behavior and configuration so that this library "just works" if you've already configured credentials that work with `docker`; however, when things don't work, a basic understanding of what's going on can help with debugging. + +The official documentation for how authentication with `docker` works is (reasonably) scattered across several different sites and GitHub repositories, so we've tried to summarize the relevant bits here. + +## tl;dr for consumers of this package + +By default, [`pkg/v1/remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote) uses [`Anonymous`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#Anonymous) credentials (i.e. _none_), which for most registries will only allow read access to public images. + +To use the credentials found in your Docker config file, you can use the [`DefaultKeychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#DefaultKeychain), e.g.: + +```go +package main + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + ref, err := name.ParseReference("registry.example.com/private/repo") + if err != nil { + panic(err) + } + + // Fetch the manifest using default credentials. + img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + panic(err) + } + + // Prints the digest of registry.example.com/private/repo + fmt.Println(img.Digest) +} +``` + +The `DefaultKeychain` will use credentials as described in your Docker config file -- usually `~/.docker/config.json`, or `%USERPROFILE%\.docker\config.json` on Windows -- or the location described by the `DOCKER_CONFIG` environment variable, if set. + +If those are not found, `DefaultKeychain` will look for credentials configured using [Podman's expectation](https://docs.podman.io/en/latest/markdown/podman-login.1.html) that these are found in `${XDG_RUNTIME_DIR}/containers/auth.json`. + +[See below](#docker-config-auth) for more information about what is configured in this file. + +## Emulating Cloud Provider Credential Helpers + +[`pkg/v1/google.Keychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#Keychain) provides a `Keychain` implementation that emulates [`docker-credential-gcr`](https://github.com/GoogleCloudPlatform/docker-credential-gcr) to find credentials in the environment. +See [`google.NewEnvAuthenticator`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#NewEnvAuthenticator) and [`google.NewGcloudAuthenticator`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/google#NewGcloudAuthenticator) for more information. + +To emulate other credential helpers without requiring them to be available as executables, [`NewKeychainFromHelper`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#NewKeychainFromHelper) provides an adapter that takes a Go implementation satisfying a subset of the [`credentials.Helper`](https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper) interface, and makes it available as a `Keychain`. + +This means that you can emulate, for example, [Amazon ECR's `docker-credential-ecr-login` credential helper](https://github.com/awslabs/amazon-ecr-credential-helper) using the same implementation: + +```go +import ( + ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" + "github.com/awslabs/amazon-ecr-credential-helper/ecr-login/api" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + // ... + ecrHelper := ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}} + img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(ecrHelper))) + if err != nil { + panic(err) + } + // ... +} +``` + +Likewise, you can emulate [Azure's ACR `docker-credential-acr-env` credential helper](https://github.com/chrismellard/docker-credential-acr-env): + +```go +import ( + "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + // ... + acrHelper := credhelper.NewACRCredentialsHelper() + img, err := remote.Get(ref, remote.WithAuthFromKeychain(authn.NewKeychainFromHelper(acrHelper))) + if err != nil { + panic(err) + } + // ... +} +``` + +<!-- TODO(jasonhall): Wrap these in docker-credential-magic and reference those from here. --> + +## Using Multiple `Keychain`s + +[`NewMultiKeychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/authn#NewMultiKeychain) allows you to specify multiple `Keychain` implementations, which will be checked in order when credentials are needed. + +For example: + +```go +kc := authn.NewMultiKeychain( + authn.DefaultKeychain, + google.Keychain, + authn.NewKeychainFromHelper(ecr.ECRHelper{ClientFactory: api.DefaultClientFactory{}}), + authn.NewKeychainFromHelper(acr.ACRCredHelper{}), +) +``` + +This multi-keychain will: + +- first check for credentials found in the Docker config file, as describe above, then +- check for GCP credentials available in the environment, as described above, then +- check for ECR credentials by emulating the ECR credential helper, then +- check for ACR credentials by emulating the ACR credential helper. + +If any keychain implementation is able to provide credentials for the request, they will be used, and further keychain implementations will not be consulted. + +If no implementations are able to provide credentials, `Anonymous` credentials will be used. + +## Docker Config Auth + +What follows attempts to gather useful information about Docker's config.json and make it available in one place. + +If you have questions, please [file an issue](https://github.com/google/go-containerregistry/issues/new). + +### Plaintext + +The config file is where your credentials are stored when you invoke `docker login`, e.g. the contents may look something like this: + +```json +{ + "auths": { + "registry.example.com": { + "auth": "QXp1cmVEaWFtb25kOmh1bnRlcjI=" + } + } +} +``` + +The `auths` map has an entry per registry, and the `auth` field contains your username and password encoded as [HTTP 'Basic' Auth](https://tools.ietf.org/html/rfc7617). + +**NOTE**: This means that your credentials are stored _in plaintext_: + +```bash +$ echo "QXp1cmVEaWFtb25kOmh1bnRlcjI=" | base64 -d +AzureDiamond:hunter2 +``` + +For what it's worth, this config file is equivalent to: + +```json +{ + "auths": { + "registry.example.com": { + "username": "AzureDiamond", + "password": "hunter2" + } + } +} +``` + +... which is useful to know if e.g. your CI system provides you a registry username and password via environment variables and you want to populate this file manually without invoking `docker login`. + +### Helpers + +If you log in like this, `docker` will warn you that you should use a [credential helper](https://docs.docker.com/engine/reference/commandline/login/#credentials-store), and you should! + +To configure a global credential helper: +```json +{ + "credsStore": "osxkeychain" +} +``` + +To configure a per-registry credential helper: +```json +{ + "credHelpers": { + "gcr.io": "gcr" + } +} +``` + +We use [`github.com/docker/cli/cli/config.Load`](https://godoc.org/github.com/docker/cli/cli/config#Load) to parse the config file and invoke any necessary credential helpers. This handles the logic of taking a [`ConfigFile`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/configfile/file.go#L25-L54) + registry domain and producing an [`AuthConfig`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L3-L22), which determines how we authenticate to the registry. + +## Credential Helpers + +The [credential helper protocol](https://github.com/docker/docker-credential-helpers) allows you to configure a binary that supplies credentials for the registry, rather than hard-coding them in the config file. + +The protocol has several verbs, but the one we most care about is `get`. + +For example, using the following config file: +```json +{ + "credHelpers": { + "gcr.io": "gcr", + "eu.gcr.io": "gcr" + } +} +``` + +To acquire credentials for `gcr.io`, we look in the `credHelpers` map to find +the credential helper for `gcr.io` is `gcr`. By appending that value to +`docker-credential-`, we can get the name of the binary we need to use. + +For this example, that's `docker-credential-gcr`, which must be on our `$PATH`. +We'll then invoke that binary to get credentials: + +```bash +$ echo "gcr.io" | docker-credential-gcr get +{"Username":"_token","Secret":"<long access token>"} +``` + +You can configure the same credential helper for multiple registries, which is +why we need to pass the domain in via STDIN, e.g. if we were trying to access +`eu.gcr.io`, we'd do this instead: + +```bash +$ echo "eu.gcr.io" | docker-credential-gcr get +{"Username":"_token","Secret":"<long access token>"} +``` + +### Debugging credential helpers + +If a credential helper is configured but doesn't seem to be working, it can be +challenging to debug. Implementing a fake credential helper lets you poke around +to make it easier to see where the failure is happening. + +This "implements" a credential helper with hard-coded values: +``` +#!/usr/bin/env bash +echo '{"Username":"<token>","Secret":"hunter2"}' +``` + + +This implements a credential helper that prints the output of +`docker-credential-gcr` to both stderr and whatever called it, which allows you +to snoop on another credential helper: +``` +#!/usr/bin/env bash +docker-credential-gcr $@ | tee >(cat 1>&2) +``` + +Put those files somewhere on your path, naming them e.g. +`docker-credential-hardcoded` and `docker-credential-tee`, then modify the +config file to use them: + +```json +{ + "credHelpers": { + "gcr.io": "tee", + "eu.gcr.io": "hardcoded" + } +} +``` + +The `docker-credential-tee` trick works with both `crane` and `docker`: + +```bash +$ crane manifest gcr.io/google-containers/pause > /dev/null +{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"} + +$ docker pull gcr.io/google-containers/pause +Using default tag: latest +{"ServerURL":"","Username":"_dcgcr_1_5_0_token","Secret":"<redacted>"} +latest: Pulling from google-containers/pause +a3ed95caeb02: Pull complete +4964c72cd024: Pull complete +Digest: sha256:a78c2d6208eff9b672de43f880093100050983047b7b0afe0217d3656e1b0d5f +Status: Downloaded newer image for gcr.io/google-containers/pause:latest +gcr.io/google-containers/pause:latest +``` + +## The Registry + +There are two methods for authenticating against a registry: +[token](https://docs.docker.com/registry/spec/auth/token/) and +[oauth2](https://docs.docker.com/registry/spec/auth/oauth/). + +Both methods are used to acquire an opaque `Bearer` token (or +[RegistryToken](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L21)) +to use in the `Authorization` header. The registry will return a `401 +Unauthorized` during the [version +check](https://github.com/opencontainers/distribution-spec/blob/2c3975d1f03b67c9a0203199038adea0413f0573/spec.md#api-version-check) +(or during normal operations) with +[Www-Authenticate](https://tools.ietf.org/html/rfc7235#section-4.1) challenge +indicating how to proceed. + +### Token + +If we get back an `AuthConfig` containing a [`Username/Password`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L5-L6) +or +[`Auth`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L7), +we'll use the token method for authentication: + +![basic](../../images/credhelper-basic.svg) + +### OAuth 2 + +If we get back an `AuthConfig` containing an [`IdentityToken`](https://github.com/docker/cli/blob/ba63a92655c0bea4857b8d6cc4991498858b3c60/cli/config/types/authconfig.go#L18) +we'll use the oauth2 method for authentication: + +![oauth](../../images/credhelper-oauth.svg) + +This happens when a credential helper returns a response with the +[`Username`](https://github.com/docker/docker-credential-helpers/blob/f78081d1f7fef6ad74ad6b79368de6348386e591/credentials/credentials.go#L16) +set to `<token>` (no, that's not a placeholder, the literal string `"<token>"`). +It is unclear why: [moby/moby#36926](https://github.com/moby/moby/issues/36926). + +We only support the oauth2 `grant_type` for `refresh_token` ([#629](https://github.com/google/go-containerregistry/issues/629)), +since it's impossible to determine from the registry response whether we should +use oauth, and the token method for authentication is widely implemented by +registries. diff --git a/pkg/authn/anon.go b/pkg/authn/anon.go new file mode 100644 index 0000000..8321495 --- /dev/null +++ b/pkg/authn/anon.go @@ -0,0 +1,26 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +// anonymous implements Authenticator for anonymous authentication. +type anonymous struct{} + +// Authorization implements Authenticator. +func (a *anonymous) Authorization() (*AuthConfig, error) { + return &AuthConfig{}, nil +} + +// Anonymous is a singleton Authenticator for providing anonymous auth. +var Anonymous Authenticator = &anonymous{} diff --git a/pkg/authn/anon_test.go b/pkg/authn/anon_test.go new file mode 100644 index 0000000..83c8214 --- /dev/null +++ b/pkg/authn/anon_test.go @@ -0,0 +1,31 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "reflect" + "testing" +) + +func TestAnonymous(t *testing.T) { + cfg, err := Anonymous.Authorization() + if err != nil { + t.Fatalf("Authorization() = %v", err) + } + want := &AuthConfig{} + if !reflect.DeepEqual(cfg, want) { + t.Errorf("Authorization(); got %v, wanted {}", cfg) + } +} diff --git a/pkg/authn/auth.go b/pkg/authn/auth.go new file mode 100644 index 0000000..0111f1a --- /dev/null +++ b/pkg/authn/auth.go @@ -0,0 +1,30 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +// auth is an Authenticator that simply returns the wrapped AuthConfig. +type auth struct { + config AuthConfig +} + +// FromConfig returns an Authenticator that just returns the given AuthConfig. +func FromConfig(cfg AuthConfig) Authenticator { + return &auth{cfg} +} + +// Authorization implements Authenticator. +func (a *auth) Authorization() (*AuthConfig, error) { + return &a.config, nil +} diff --git a/pkg/authn/authn.go b/pkg/authn/authn.go new file mode 100644 index 0000000..172d218 --- /dev/null +++ b/pkg/authn/authn.go @@ -0,0 +1,115 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" +) + +// Authenticator is used to authenticate Docker transports. +type Authenticator interface { + // Authorization returns the value to use in an http transport's Authorization header. + Authorization() (*AuthConfig, error) +} + +// AuthConfig contains authorization information for connecting to a Registry +// Inlined what we use from github.com/docker/cli/cli/config/types +type AuthConfig struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` + + // IdentityToken is used to authenticate the user and get + // an access token for the registry. + IdentityToken string `json:"identitytoken,omitempty"` + + // RegistryToken is a bearer token to be sent to a registry + RegistryToken string `json:"registrytoken,omitempty"` +} + +// This is effectively a copy of the type AuthConfig. This simplifies +// JSON unmarshalling since AuthConfig methods are not inherited +type authConfig AuthConfig + +// UnmarshalJSON implements json.Unmarshaler +func (a *AuthConfig) UnmarshalJSON(data []byte) error { + var shadow authConfig + err := json.Unmarshal(data, &shadow) + if err != nil { + return err + } + + *a = (AuthConfig)(shadow) + + if len(shadow.Auth) != 0 { + var derr error + a.Username, a.Password, derr = decodeDockerConfigFieldAuth(shadow.Auth) + if derr != nil { + err = fmt.Errorf("unable to decode auth field: %w", derr) + } + } else if len(a.Username) != 0 && len(a.Password) != 0 { + a.Auth = encodeDockerConfigFieldAuth(shadow.Username, shadow.Password) + } + + return err +} + +// MarshalJSON implements json.Marshaler +func (a AuthConfig) MarshalJSON() ([]byte, error) { + shadow := (authConfig)(a) + shadow.Auth = encodeDockerConfigFieldAuth(shadow.Username, shadow.Password) + return json.Marshal(shadow) +} + +// decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a +// username and a password. The format of the auth field is base64(<username>:<password>). +// +// From https://github.com/kubernetes/kubernetes/blob/75e49ec824b183288e1dbaccfd7dbe77d89db381/pkg/credentialprovider/config.go +// Copyright 2014 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 +func decodeDockerConfigFieldAuth(field string) (username, password string, err error) { + var decoded []byte + // StdEncoding can only decode padded string + // RawStdEncoding can only decode unpadded string + if strings.HasSuffix(strings.TrimSpace(field), "=") { + // decode padded data + decoded, err = base64.StdEncoding.DecodeString(field) + } else { + // decode unpadded data + decoded, err = base64.RawStdEncoding.DecodeString(field) + } + + if err != nil { + return + } + + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) != 2 { + err = fmt.Errorf("must be formatted as base64(username:password)") + return + } + + username = parts[0] + password = parts[1] + + return +} + +func encodeDockerConfigFieldAuth(username, password string) string { + return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) +} diff --git a/pkg/authn/authn_test.go b/pkg/authn/authn_test.go new file mode 100644 index 0000000..f191acd --- /dev/null +++ b/pkg/authn/authn_test.go @@ -0,0 +1,148 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestAuthConfigMarshalJSON(t *testing.T) { + cases := []struct { + name string + config AuthConfig + json string + }{{ + name: "auth field is calculated", + config: AuthConfig{ + Username: "user", + Password: "pass", + IdentityToken: "id", + RegistryToken: "reg", + }, + json: `{"username":"user","password":"pass","auth":"dXNlcjpwYXNz","identitytoken":"id","registrytoken":"reg"}`, + }, { + name: "auth field replaced", + config: AuthConfig{ + Username: "user", + Password: "pass", + Auth: "blah", + IdentityToken: "id", + RegistryToken: "reg", + }, + json: `{"username":"user","password":"pass","auth":"dXNlcjpwYXNz","identitytoken":"id","registrytoken":"reg"}`, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + bytes, err := json.Marshal(&tc.config) + + if err != nil { + t.Fatal("Marshal() =", err) + } + + if diff := cmp.Diff(tc.json, string(bytes)); diff != "" { + t.Error("json output diff (-want, +got): ", diff) + } + }) + } +} + +func TestAuthConfigUnmarshalJSON(t *testing.T) { + cases := []struct { + name string + json string + err string + want AuthConfig + }{{ + name: "valid config no auth", + json: `{ + "username": "user", + "password": "pass", + "identitytoken": "id", + "registrytoken": "reg" + }`, + want: AuthConfig{ + // Auth value is set based on username and password + Auth: "dXNlcjpwYXNz", + Username: "user", + Password: "pass", + IdentityToken: "id", + RegistryToken: "reg", + }, + }, { + name: "bad json input", + json: `{"username":true}`, + err: "json: cannot unmarshal", + }, { + name: "auth is base64", + json: `{ "auth": "dXNlcjpwYXNz" }`, // user:pass + want: AuthConfig{ + Username: "user", + Password: "pass", + Auth: "dXNlcjpwYXNz", + }, + }, { + name: "auth field overrides others", + json: `{ "auth": "dXNlcjpwYXNz", "username":"foo", "password":"bar" }`, // user:pass + want: AuthConfig{ + Username: "user", + Password: "pass", + Auth: "dXNlcjpwYXNz", + }, + }, { + name: "auth is base64 padded", + json: `{ "auth": "dXNlcjpwYXNzd29yZA==" }`, // user:password + want: AuthConfig{ + Username: "user", + Password: "password", + Auth: "dXNlcjpwYXNzd29yZA==", + }, + }, { + name: "auth is not base64", + json: `{ "auth": "bad-auth-bad" }`, + err: "unable to decode auth field", + }, { + name: "decoded auth is not valid", + json: `{ "auth": "Zm9vYmFy" }`, + err: "unable to decode auth field: must be formatted as base64(username:password)", + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var got AuthConfig + err := json.Unmarshal([]byte(tc.json), &got) + if tc.err != "" && err == nil { + t.Fatal("no error occurred expected:", tc.err) + } else if tc.err != "" && err != nil { + if !strings.HasPrefix(err.Error(), tc.err) { + t.Fatalf("expected err %q to have prefix %q", err, tc.err) + } + return + } + + if err != nil { + t.Fatal("Unmarshal()=", err) + } + + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Fatal("unexpected diff (-want, +got)\n", diff) + } + }) + } +} diff --git a/pkg/authn/basic.go b/pkg/authn/basic.go new file mode 100644 index 0000000..500cb66 --- /dev/null +++ b/pkg/authn/basic.go @@ -0,0 +1,29 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +// Basic implements Authenticator for basic authentication. +type Basic struct { + Username string + Password string +} + +// Authorization implements Authenticator. +func (b *Basic) Authorization() (*AuthConfig, error) { + return &AuthConfig{ + Username: b.Username, + Password: b.Password, + }, nil +} diff --git a/pkg/authn/basic_test.go b/pkg/authn/basic_test.go new file mode 100644 index 0000000..aecbe15 --- /dev/null +++ b/pkg/authn/basic_test.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "reflect" + "testing" +) + +func TestBasic(t *testing.T) { + basic := &Basic{Username: "foo", Password: "bar"} + + got, err := basic.Authorization() + if err != nil { + t.Fatalf("Authorization() = %v", err) + } + want := &AuthConfig{Username: "foo", Password: "bar"} + if !reflect.DeepEqual(got, want) { + t.Errorf("Authorization(); got %v, want %v", got, want) + } +} diff --git a/pkg/authn/bearer.go b/pkg/authn/bearer.go new file mode 100644 index 0000000..4cf86df --- /dev/null +++ b/pkg/authn/bearer.go @@ -0,0 +1,27 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +// Bearer implements Authenticator for bearer authentication. +type Bearer struct { + Token string `json:"token"` +} + +// Authorization implements Authenticator. +func (b *Bearer) Authorization() (*AuthConfig, error) { + return &AuthConfig{ + RegistryToken: b.Token, + }, nil +} diff --git a/pkg/authn/bearer_test.go b/pkg/authn/bearer_test.go new file mode 100644 index 0000000..7d6b26b --- /dev/null +++ b/pkg/authn/bearer_test.go @@ -0,0 +1,31 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "testing" +) + +func TestBearer(t *testing.T) { + anon := &Bearer{Token: "bazinga"} + + auth, err := anon.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if got, want := auth.RegistryToken, "bazinga"; got != want { + t.Errorf("Authorization(); got %v, want %v", got, want) + } +} diff --git a/pkg/authn/doc.go b/pkg/authn/doc.go new file mode 100644 index 0000000..c2a5fc0 --- /dev/null +++ b/pkg/authn/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package authn defines different methods of authentication for +// talking to a container registry. +package authn diff --git a/pkg/authn/github/keychain.go b/pkg/authn/github/keychain.go new file mode 100644 index 0000000..97ad34e --- /dev/null +++ b/pkg/authn/github/keychain.go @@ -0,0 +1,59 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package github provides a keychain for the GitHub Container Registry. +package github + +import ( + "net/url" + "os" + + "github.com/google/go-containerregistry/pkg/authn" +) + +const ghcrHostname = "ghcr.io" + +// Keychain exports an instance of the GitHub Keychain. +// +// This keychain matches on requests for ghcr.io and provides the value of the +// environment variable $GITHUB_TOKEN, if it's set. +var Keychain authn.Keychain = githubKeychain{} + +type githubKeychain struct{} + +func (githubKeychain) Resolve(r authn.Resource) (authn.Authenticator, error) { + serverURL, err := url.Parse("https://" + r.String()) + if err != nil { + return authn.Anonymous, nil + } + if serverURL.Hostname() == ghcrHostname { + username := os.Getenv("GITHUB_ACTOR") + if username == "" { + username = "unset" + } + if tok := os.Getenv("GITHUB_TOKEN"); tok != "" { + return githubAuthenticator{username, tok}, nil + } + } + return authn.Anonymous, nil +} + +type githubAuthenticator struct{ username, password string } + +func (g githubAuthenticator) Authorization() (*authn.AuthConfig, error) { + return &authn.AuthConfig{ + Username: g.username, + Password: g.password, + }, nil +} diff --git a/pkg/authn/github/keychain_test.go b/pkg/authn/github/keychain_test.go new file mode 100644 index 0000000..c3e858a --- /dev/null +++ b/pkg/authn/github/keychain_test.go @@ -0,0 +1,112 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package github + +import ( + "os" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" +) + +// TestKeychain checks that the keychain resolves when $GITHUB_TOKEN is set and +// the request is for GHCR. +func TestKeychain(t *testing.T) { + username, tok := "octocat", "my-token" + os.Setenv("GITHUB_ACTOR", username) + os.Setenv("GITHUB_TOKEN", tok) + got, err := Keychain.Resolve(resource("ghcr.io/my/repo")) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got == authn.Anonymous { + t.Fatalf("Got anonymous, wanted authenticator") + } + + auth, err := got.Authorization() + if err != nil { + t.Fatalf("Authorization: %v", err) + } + if auth.Username != username { + t.Errorf("Got username %q, want %q", auth.Username, username) + } + if auth.Password != tok { + t.Errorf("Got password %q, want %q", auth.Password, tok) + } +} + +// TestKeychainUsernameUnset checks that the keychain resolves an "unset" +// username when $GITHUB_ACTOR is not set. +func TestKeychainUsernameUnset(t *testing.T) { + tok := "my-token" + os.Unsetenv("GITHUB_ACTOR") + os.Setenv("GITHUB_TOKEN", tok) + got, err := Keychain.Resolve(resource("ghcr.io/my/repo")) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got == authn.Anonymous { + t.Fatalf("Got anonymous, wanted authenticator") + } + + auth, err := got.Authorization() + if err != nil { + t.Fatalf("Authorization: %v", err) + } + if auth.Username != "unset" { + t.Errorf("Got username %q, want unset", auth.Username) + } + if auth.Password != tok { + t.Errorf("Got password %q, want %q", auth.Password, tok) + } +} + +// TestKeychainUnset checks that the keychain doesn't resolve when the +// environment variable is unset. +func TestKeychainUnset(t *testing.T) { + os.Unsetenv("GITHUB_TOKEN") + + got, err := Keychain.Resolve(resource("ghcr.io/my/repo")) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != authn.Anonymous { + t.Errorf("Resolve(ghcr.io) got %v, want Anonymous", got) + } +} + +// TestNoMatch checks that the keychain doesn't resolve for non-GHCR registries. +func TestNoMatch(t *testing.T) { + os.Setenv("GITHUB_TOKEN", "my-token") + for _, s := range []string{ + "gcr.io", + "example.com", + "ghcr.io.example.com", + "invalid-domain-name -- %U)(@*)(%*)@(*#%@", + } { + got, err := Keychain.Resolve(resource(s)) + if err != nil { + t.Fatalf("Resolve: %v", err) + } + if got != authn.Anonymous { + t.Errorf("Resolve(%q) got %v, want Anonymous", s, got) + } + } +} + +type resource string + +func (r resource) String() string { return string(r) } +func (r resource) RegistryStr() string { return string(r) } diff --git a/pkg/authn/k8schain/README.md b/pkg/authn/k8schain/README.md new file mode 100644 index 0000000..0bf4371 --- /dev/null +++ b/pkg/authn/k8schain/README.md @@ -0,0 +1,49 @@ +# `k8schain` + +This is an implementation of the [`authn.Keychain`](https://godoc.org/github.com/google/go-containerregistry/authn#Keychain) interface loosely based on the authentication semantics used by the Kubelet when performing the pull of a Pod's images. + +This keychain supports passing a Kubernetes Service Account and some ImagePullSecrets which may represent registry credentials. + +In addition to those, the keychain also includes cloud-specific credential helpers for Google Container Registry (and Artifact Registry), Azure Container Registry, and Amazon AWS Elasic Container Registry. +This means that if the keychain is used from within Kubernetes services on those clouds (GKE, AKS, EKS), any available service credentials will be discovered and used. + +In general this keychain should be used when the code is expected to run in a Kubernetes cluster, and especially when it will run in one of those clouds. +To get a cloud-agnostic keychain, use [`pkg/authn/kubernetes`](../kubernetes) instead. + +To get only cloud-aware keychains, use [`google.Keychain`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google#Keychain), or [`pkg/authn.NewKeychainFromHelper`](https://godoc.org/github.com/google/go-containerregistry/pkg/authn#NewKeychainFromHelper) with a cloud credential helper implementation -- see the implementation of `k8schain.NewNoClient` for more details. + +## Usage + +### Creating a keychain + +A `k8schain` keychain can be built via one of: + +```go +// client is a kubernetes.Interface +kc, err := k8schain.New(ctx, client, k8schain.Options{}) +... + +// This method is suitable for use by controllers or other in-cluster processes. +kc, err := k8schain.NewInCluster(ctx, k8schain.Options{}) +... +``` + +### Using the keychain + +The `k8schain` keychain can be used directly as an `authn.Keychain`, e.g. + +```go +auth, err := kc.Resolve(registry) +if err != nil { + ... +} +``` + +Or, with the [`remote.WithAuthFromKeychain`](https://pkg.go.dev/github.com/google/go-containerregistry/pkg/v1/remote#WithAuthFromKeychain) option: + +```go +img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc)) +if err != nil { + ... +} +``` diff --git a/pkg/authn/k8schain/doc.go b/pkg/authn/k8schain/doc.go new file mode 100644 index 0000000..c9ae7f1 --- /dev/null +++ b/pkg/authn/k8schain/doc.go @@ -0,0 +1,18 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package k8schain exposes an implementation of the authn.Keychain interface +// based on the semantics the Kubelet follows when pulling the images for a +// Pod in Kubernetes. +package k8schain diff --git a/pkg/authn/k8schain/go.mod b/pkg/authn/k8schain/go.mod new file mode 100644 index 0000000..b49beac --- /dev/null +++ b/pkg/authn/k8schain/go.mod @@ -0,0 +1,96 @@ +module github.com/google/go-containerregistry/pkg/authn/k8schain + +go 1.18 + +replace ( + github.com/google/go-containerregistry => ../../../ + github.com/google/go-containerregistry/pkg/authn/kubernetes => ../kubernetes/ +) + +require ( + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 + github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 + github.com/google/go-containerregistry v0.13.0 + github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20230309011546-ff810c186c77 + k8s.io/api v0.26.2 + k8s.io/client-go v0.26.2 +) + +require ( + cloud.google.com/go/compute v1.18.0 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.28 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect + github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect + github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.17.5 // indirect + github.com/aws/aws-sdk-go-v2/config v1.18.15 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.13.15 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 // indirect + github.com/aws/smithy-go v1.13.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/docker/cli v23.0.1+incompatible // indirect + github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/docker v23.0.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.2 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apimachinery v0.26.2 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect + k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/pkg/authn/k8schain/go.sum b/pkg/authn/k8schain/go.sum new file mode 100644 index 0000000..e5cb660 --- /dev/null +++ b/pkg/authn/k8schain/go.sum @@ -0,0 +1,364 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.18.0 h1:FEigFqoDbys2cvFkZ9Fjq4gnHBP55anJ0yQyau2f9oY= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.22 h1:/GblQdIudfEM3AWWZ0mrYJQSd7JS4S/Mbzh6F0ov0Xc= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 h1:wkAZRgT/pn8HhFyzfe9UnqOjJYqlembgCTi72Bm/xKk= +github.com/Azure/go-autorest/autorest/azure/auth v0.5.12/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 h1:w77/uPk80ZET2F+AfQExZyEWtn+0Rk/uw17m9fv5Ajc= +github.com/Azure/go-autorest/autorest/azure/cli v0.4.6/go.mod h1:piCfgPho7BiIDdEQ1+g4VmKyD5y+p/XtSNqE6Hc4QD0= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aws/aws-sdk-go-v2 v1.17.5 h1:TzCUW1Nq4H8Xscph5M/skINUitxM5UBAyvm2s7XBzL4= +github.com/aws/aws-sdk-go-v2 v1.17.5/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2/config v1.18.15 h1:509yMO0pJUGUugBP2H9FOFyV+7Mz7sRR+snfDN5W4NY= +github.com/aws/aws-sdk-go-v2/config v1.18.15/go.mod h1:vS0tddZqpE8cD9CyW0/kITHF5Bq2QasW9Y1DFHD//O0= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15 h1:0rZQIi6deJFjOEgHI9HI2eZcLPPEGQPictX66oRFLL8= +github.com/aws/aws-sdk-go-v2/credentials v1.13.15/go.mod h1:vRMLMD3/rXU+o6j2MW5YefrGMBmdTvkLLGqFwMLBHQc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23 h1:Kbiv9PGnQfG/imNI4L/heyUXvzKmcWSBeDvkrQz5pFc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.23/go.mod h1:mOtmAg65GT1HIL/HT/PynwPbS+UG0BgCZ6vhkPqnxWo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29 h1:9/aKwwus0TQxppPXFmf010DFrE+ssSbzroLVYINA+xE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.29/go.mod h1:Dip3sIGv485+xerzVv24emnjX5Sg88utCL8fwGmCeWg= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23 h1:b/Vn141DBuLVgXbhRWIrl9g+ww7G+ScV5SzniWR13jQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.23/go.mod h1:mr6c4cHC+S/MMkrjtSlG4QA36kOznDep+0fga5L/fGQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30 h1:IVx9L7YFhpPq0tTnGo8u8TpluFu7nAn9X3sUDMb11c0= +github.com/aws/aws-sdk-go-v2/internal/ini v1.3.30/go.mod h1:vsbq62AOBwQ1LJ/GWKFxX8beUEYeRp/Agitrxee2/qM= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5 h1:tGA4ZoAsrYhGBypKAo2jwoX/Z5ponBZOTEUMNN/rHP4= +github.com/aws/aws-sdk-go-v2/service/ecr v1.18.5/go.mod h1:cDZh+PHP8Adt9E0zfZT9cK4qadbtIuU/czLpEJtm4wc= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4 h1:6OBVD6KE4gLReaNfG7CSXFvNIVqKIqrywRcG1kUKr4M= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.15.4/go.mod h1:gUxgbzXs+gHsj/6al9dzzoByeSrEl03Oj4iJBu/m/Rk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23 h1:QoOybhwRfciWUBbZ0gp9S7XaDnCuSTeK/fySB99V1ls= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.23/go.mod h1:9uPh+Hrz2Vn6oMnQYiUi/zbh3ovbnQk19YKINkQny44= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4 h1:qJdM48OOLl1FBSzI7ZrA1ZfLwOyCYqkXV5lko1hYDBw= +github.com/aws/aws-sdk-go-v2/service/sso v1.12.4/go.mod h1:jtLIhd+V+lft6ktxpItycqHqiVXrPIRjWIsFIlzMriw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4 h1:YRkWXQveFb0tFC0TLktmmhGsOcCgLwvq88MC2al47AA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.4/go.mod h1:zVwRrfdSmbRZWkUkWjOItY7SOalnFnq/Yg2LVPqDjwc= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5 h1:L1600eLr0YvTT7gNh3Ni24yGI7NSHkq9Gp62vijPRCs= +github.com/aws/aws-sdk-go-v2/service/sts v1.18.5/go.mod h1:1mKZHLLpDMHTNSYPJ7qrcnCQdHCWsNQaT0xRvq2u80s= +github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1 h1:uQhxQriOPUu/knXSPM7D/VyS3GMz+4wsE43eB8f9ojg= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230228174139-39c3d18f0af1/go.mod h1:/JmJjW2NJpzRSI3pOxQPC6eOD/tR8SfOA9X1FurmzXI= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= +github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589/go.mod h1:OuDyvmLnMCwa2ep4Jkm6nyA0ocJuZlGyk2gGseVzERM= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= +github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= +k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= +k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= +k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= +k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= +k8s.io/utils v0.0.0-20230308161112-d77c459e9343 h1:m7tbIjXGcGIAtpmQr7/NAi7RsWoW3E7Zcm4jI1HicTc= +k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/authn/k8schain/k8schain.go b/pkg/authn/k8schain/k8schain.go new file mode 100644 index 0000000..8ecbd5f --- /dev/null +++ b/pkg/authn/k8schain/k8schain.go @@ -0,0 +1,105 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package k8schain + +import ( + "context" + "io" + + ecr "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" + "github.com/chrismellard/docker-credential-acr-env/pkg/credhelper" + "github.com/google/go-containerregistry/pkg/authn" + kauth "github.com/google/go-containerregistry/pkg/authn/kubernetes" + "github.com/google/go-containerregistry/pkg/v1/google" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +var ( + amazonKeychain authn.Keychain = authn.NewKeychainFromHelper(ecr.NewECRHelper(ecr.WithLogger(io.Discard))) + azureKeychain authn.Keychain = authn.NewKeychainFromHelper(credhelper.NewACRCredentialsHelper()) +) + +// Options holds configuration data for guiding credential resolution. +type Options = kauth.Options + +// New returns a new authn.Keychain suitable for resolving image references as +// scoped by the provided Options. It speaks to Kubernetes through the provided +// client interface. +func New(ctx context.Context, client kubernetes.Interface, opt Options) (authn.Keychain, error) { + k8s, err := kauth.New(ctx, client, kauth.Options(opt)) + if err != nil { + return nil, err + } + + return authn.NewMultiKeychain( + k8s, + authn.DefaultKeychain, + google.Keychain, + amazonKeychain, + azureKeychain, + ), nil +} + +// NewInCluster returns a new authn.Keychain suitable for resolving image references as +// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster +// authentication. +func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) { + clusterConfig, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + + client, err := kubernetes.NewForConfig(clusterConfig) + if err != nil { + return nil, err + } + return New(ctx, client, opt) +} + +// NewNoClient returns a new authn.Keychain that supports the portions of the K8s keychain +// that don't read ImagePullSecrets. This limits it to roughly the Node-identity-based +// authentication schemes in Kubernetes pkg/credentialprovider. This version of the +// k8schain drops the requirement that we run as a K8s serviceaccount with access to all +// of the on-cluster secrets. This drop in fidelity also diminishes its value as a stand-in +// for Kubernetes authentication, but this actually targets a different use-case. What +// remains is an interesting sweet spot: this variant can serve as a credential provider +// for all of the major public clouds, but in library form (vs. an executable you exec). +func NewNoClient(ctx context.Context) (authn.Keychain, error) { + return authn.NewMultiKeychain( + authn.DefaultKeychain, + google.Keychain, + amazonKeychain, + azureKeychain, + ), nil +} + +// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as +// scoped by the pull secrets. +func NewFromPullSecrets(ctx context.Context, pullSecrets []corev1.Secret) (authn.Keychain, error) { + k8s, err := kauth.NewFromPullSecrets(ctx, pullSecrets) + if err != nil { + return nil, err + } + + return authn.NewMultiKeychain( + k8s, + authn.DefaultKeychain, + google.Keychain, + amazonKeychain, + azureKeychain, + ), nil +} diff --git a/pkg/authn/k8schain/tests/explicit/main.go b/pkg/authn/k8schain/tests/explicit/main.go new file mode 100644 index 0000000..744320e --- /dev/null +++ b/pkg/authn/k8schain/tests/explicit/main.go @@ -0,0 +1,52 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + ref, err := name.NewTag("gcr.io/build-crd-testing/secret-sauce:latest") + if err != nil { + log.Fatalf("NewTag() = %v", err) + } + + kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{ + Namespace: "explicit-namespace", + ImagePullSecrets: []string{ + "explicit-secret", + }, + }) + if err != nil { + log.Fatalf("k8schain.New() = %v", err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc)) + if err != nil { + log.Fatalf("remote.Image() = %v", err) + } + + digest, err := img.Digest() + if err != nil { + log.Fatalf("Digest() = %v", err) + } + log.Printf("got digest: %v", digest) +} diff --git a/pkg/authn/k8schain/tests/explicit/test.yaml b/pkg/authn/k8schain/tests/explicit/test.yaml new file mode 100644 index 0000000..10cdfba --- /dev/null +++ b/pkg/authn/k8schain/tests/explicit/test.yaml @@ -0,0 +1,59 @@ +# Copyright 2018 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: Namespace +metadata: + name: explicit-namespace +--- +apiVersion: v1 +kind: Secret +metadata: + name: explicit-secret + namespace: explicit-namespace +type: kubernetes.io/dockercfg +data: + # This service account is JUST a storage reader on gcr.io/build-crd-testing + .dockercfg: eyJodHRwczovL2djci5pbyI6eyJ1c2VybmFtZSI6Il9qc29uX2tleSIsInBhc3N3b3JkIjoie1xuICBcInR5cGVcIjogXCJzZXJ2aWNlX2FjY291bnRcIixcbiAgXCJwcm9qZWN0X2lkXCI6IFwiYnVpbGQtY3JkLXRlc3RpbmdcIixcbiAgXCJwcml2YXRlX2tleV9pZFwiOiBcIjA1MDJhNDFhODEyZmI2NGNlNTZhNjhlYzU4MzJhYjBiYTExYzExZTZcIixcbiAgXCJwcml2YXRlX2tleVwiOiBcIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxcbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzlYNEVZT0FSYnhRTThcXG5EMnhYY2FaVGsrZ1k4ZWp1OTh0THFDUXFUckdNVzlSZVQyeE9ZNUF5Z2FsUFArcDd5WEVja3dCRC9IaE0wZ2xJXFxuN01UTGRlZUtXcityQTFMd0haeVdGVzdIME9uZjd3bllIRUhMV1VtYzNCQ09SRUR0SFJaN1pyUEJmMUhUQUEvM1xcbk1uVzVsWkhTTjlvanpTU0Z3NkFWdTZqNmF4YkJJSUo3NTRMcmdLZUFZdXJ3ZklRMlJMVHUyMDFrMklxTFliaGJcXG4zbVNWRzVSK3RiS3oxQ3ZNNTNuSENiN0NmdVZlV3NyQThrazd4SHJyTFFLTW1JOXYyc2dSdWd5TUF6d3ovNnpOXFxuaDUvaU14eGdlcTVXOHhrVngzSjJuWThKSmRIYWYvVDZBR3NPTkVvNDNweGVpUVZqblJmL0tuMTBUQ2MyRXNJWVxcblM0OVVzWjdCQWdNQkFBRUNnZ0VBQXVwbGR1a0NRUXVENVUvZ2FtSHQ3R2dXM0FNVjE4ZXFuSG5DYTJqbGFoK1NcXG5BZVVHbmhnSmpOdkUrcE1GbFN2NXVmMnAySzRlZC9veEQ2K0NwOVpYRFJqZ3ZmdEl5cWpsemJ3dkZjZ3p3TnVEXFxueWdVa3VwN0hlY0RzRDhUdGVBb2JUL1Zwd3E2ektNckJ3Q3ZOa3Z5NmJWbG9FajV4M2JYc2F4ZTk1RE8veXB1NlxcbncwVzk3enh3d0RKWTZLUWNJV01qaHJHeHZ3WDduaVVDZU00bGVXQkR5R3R3MXplSm40aEVjNk4zYWpRYWNYS2NcXG4rNFFseGNpYW1ZcVFXYlBudHhXUWhoUXpjSFdMaTJsOWNGYlpENyt1SkxGNGlONnk4bVZOVTNLM0sxYlJZclNEXFxuUlVwM2FVVkJYbUZnK1ovMnB1VkwrbVUzajNMTFdZeUJPa2V2dU9tZGdRS0JnUURlM0dJUWt5V0lTMTRUZE1PU1xcbkJpS0JDRHk4aDk2ZWhMMEhrRGJ5T2tTdFBLZEY5cHVFeFp4aHk3b2pIQ0lNNUZWcnBSTjI1cDRzRXp3RmFjK3ZcXG5KSUZnRXZxN21YZm1YaVhJTmllUG9FUWFDbm54RHhXZ21yMEhVS0VtUzlvTWRnTGNHVStrQ1ZHTnN6N0FPdW0wXFxuS3FZM3MyMlE5bFE2N0ZPeXFpdThXRlE3UVFLQmdRRFppRmhURVprUEVjcVpqbndKcFRCNTZaV1A5S1RzbFpQN1xcbndVNGJ6aTZ5K21leWYzTUorNEwyU3lIYzNjcFNNYmp0Tk9aQ3Q0N2I5MDhGVW1MWFVHTmhjd3VaakVReEZleTBcXG5tNDFjUzVlNFA0OWI5bjZ5TEJqQnJCb3FzMldCYWwyZWdkaE5KU3NDV29pWlA4L1pUOGVnWHZoN2I5MWp6b0syXFxucTJQVW1BNERnUUtCZ0FXTDJJanZFSTBPeXgyUzExY24vZTNXSmFUUGdOUFRHOTAzVXBhK3FuemhPSXgrTWFxaFxcblBGNFdzdUF5MEFvZ0dKd2dOSmJOOEh2S1VzRVR2QTV3eXlOMzlYTjd3MGNoYXJGTDM3b3NVK1dPQXpEam5qY3NcXG5BcTVPN0dQR21YdWI2RUJRQlBKaEpQMXd5NHYvSzFmSGcvRjQ3cTRmNDBMQUpPa2FZUkpENUh6QkFvR0JBTlVoXFxubklCUEpxcTRJTXZRNmNDOWc4QisxeFlEZWE5L1lrMXcrU21QR3Z3ckVYeTNHS3g0SzdsS3BiUHo3bTRYMzNzeFxcbnNFVS8rWTJWUW13UmExeFFtLzUzcks3VjJsNUpmL0Q0MDBqUm02WmZTQU92Z0RUcnRablVHSk1yejlFN3VOdzdcXG5sZ1VIM0pyaXZ5Ri9meE1JOHFzelFid1hQMCt4bnlxQXhFQWdkdUtCQW9HQUlNK1BTTllXQ1pYeERwU0hJMThkXFxuaktrb0FidzJNb3l3UUlsa2V1QW4xZFhGYWQxenNYUUdkVHJtWHl2N05QUCs4R1hCa25CTGkzY3Z4VGlsSklTeVxcbnVjTnJDTWlxTkFTbi9kcTdjV0RGVUFCZ2pYMTZKSDJETkZaL2wvVVZGM05EQUpqWENzMVg3eUlKeVhCNm94L3pcXG5hU2xxbElNVjM1REJEN3F4Unl1S3Nnaz1cXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXFxuXCIsXG4gIFwiY2xpZW50X2VtYWlsXCI6IFwicHVsbC1zZWNyZXQtdGVzdGluZ0BidWlsZC1jcmQtdGVzdGluZy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbVwiLFxuICBcImNsaWVudF9pZFwiOiBcIjEwNzkzNTg2MjAzMzAyNTI1MTM1MlwiLFxuICBcImF1dGhfdXJpXCI6IFwiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGhcIixcbiAgXCJ0b2tlbl91cmlcIjogXCJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW5cIixcbiAgXCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmxcIjogXCJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHNcIixcbiAgXCJjbGllbnRfeDUwOV9jZXJ0X3VybFwiOiBcImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvcHVsbC1zZWNyZXQtdGVzdGluZyU0MGJ1aWxkLWNyZC10ZXN0aW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tXCJcbn0iLCJlbWFpbCI6Im5vcmVwbHlAZ29vZ2xlLmNvbSIsImF1dGgiOiJYMnB6YjI1ZmEyVjVPbnNLSUNBaWRIbHdaU0k2SUNKelpYSjJhV05sWDJGalkyOTFiblFpTEFvZ0lDSndjbTlxWldOMFgybGtJam9nSW1KMWFXeGtMV055WkMxMFpYTjBhVzVuSWl3S0lDQWljSEpwZG1GMFpWOXJaWGxmYVdRaU9pQWlNRFV3TW1FME1XRTRNVEptWWpZMFkyVTFObUUyT0dWak5UZ3pNbUZpTUdKaE1URmpNVEZsTmlJc0NpQWdJbkJ5YVhaaGRHVmZhMlY1SWpvZ0lpMHRMUzB0UWtWSFNVNGdVRkpKVmtGVVJTQkxSVmt0TFMwdExWeHVUVWxKUlhaUlNVSkJSRUZPUW1kcmNXaHJhVWM1ZHpCQ1FWRkZSa0ZCVTBOQ1MyTjNaMmRUYWtGblJVRkJiMGxDUVZGRE9WZzBSVmxQUVZKaWVGRk5PRnh1UkRKNFdHTmhXbFJySzJkWk9HVnFkVGs0ZEV4eFExRnhWSEpIVFZjNVVtVlVNbmhQV1RWQmVXZGhiRkJRSzNBM2VWaEZZMnQzUWtRdlNHaE5NR2RzU1Z4dU4wMVVUR1JsWlV0WGNpdHlRVEZNZDBoYWVWZEdWemRJTUU5dVpqZDNibGxJUlVoTVYxVnRZek5DUTA5U1JVUjBTRkphTjFweVVFSm1NVWhVUVVFdk0xeHVUVzVYTld4YVNGTk9PVzlxZWxOVFJuYzJRVloxTm1vMllYaGlRa2xKU2pjMU5FeHlaMHRsUVZsMWNuZG1TVkV5VWt4VWRUSXdNV3N5U1hGTVdXSm9ZbHh1TTIxVFZrYzFVaXQwWWt0Nk1VTjJUVFV6YmtoRFlqZERablZXWlZkemNrRTRhMnMzZUVoeWNreFJTMDF0U1RsMk1uTm5VblZuZVUxQmVuZDZMelo2VGx4dWFEVXZhVTE0ZUdkbGNUVlhPSGhyVm5nelNqSnVXVGhLU21SSVlXWXZWRFpCUjNOUFRrVnZORE53ZUdWcFVWWnFibEptTDB0dU1UQlVRMk15UlhOSldWeHVVelE1VlhOYU4wSkJaMDFDUVVGRlEyZG5SVUZCZFhCc1pIVnJRMUZSZFVRMVZTOW5ZVzFJZERkSFoxY3pRVTFXTVRobGNXNUlia05oTW1wc1lXZ3JVMXh1UVdWVlIyNW9aMHBxVG5aRkszQk5SbXhUZGpWMVpqSndNa3MwWldRdmIzaEVOaXREY0RsYVdFUlNhbWQyWm5SSmVYRnFiSHBpZDNaR1kyZDZkMDUxUkZ4dWVXZFZhM1Z3TjBobFkwUnpSRGhVZEdWQmIySlVMMVp3ZDNFMmVrdE5ja0ozUTNaT2EzWjVObUpXYkc5RmFqVjRNMkpZYzJGNFpUazFSRTh2ZVhCMU5seHVkekJYT1RkNmVIZDNSRXBaTmt0UlkwbFhUV3BvY2tkNGRuZFlOMjVwVlVObFRUUnNaVmRDUkhsSGRIY3hlbVZLYmpSb1JXTTJUak5oYWxGaFkxaExZMXh1S3pSUmJIaGphV0Z0V1hGUlYySlFiblI0VjFGb2FGRjZZMGhYVEdreWJEbGpSbUphUkRjcmRVcE1SalJwVGpaNU9HMVdUbFV6U3pOTE1XSlNXWEpUUkZ4dVVsVndNMkZWVmtKWWJVWm5LMW92TW5CMVZrd3JiVlV6YWpOTVRGZFplVUpQYTJWMmRVOXRaR2RSUzBKblVVUmxNMGRKVVd0NVYwbFRNVFJVWkUxUFUxeHVRbWxMUWtORWVUaG9PVFpsYUV3d1NHdEVZbmxQYTFOMFVFdGtSamx3ZFVWNFduaG9lVGR2YWtoRFNVMDFSbFp5Y0ZKT01qVndOSE5GZW5kR1lXTXJkbHh1U2tsR1owVjJjVGR0V0dadFdHbFlTVTVwWlZCdlJWRmhRMjV1ZUVSNFYyZHRjakJJVlV0RmJWTTViMDFrWjB4alIxVXJhME5XUjA1emVqZEJUM1Z0TUZ4dVMzRlpNM015TWxFNWJGRTJOMFpQZVhGcGRUaFhSbEUzVVZGTFFtZFJSRnBwUm1oVVJWcHJVRVZqY1ZwcWJuZEtjRlJDTlRaYVYxQTVTMVJ6YkZwUU4xeHVkMVUwWW5wcE5ua3JiV1Y1WmpOTlNpczBUREpUZVVoak0yTndVMDFpYW5ST1QxcERkRFEzWWprd09FWlZiVXhZVlVkT2FHTjNkVnBxUlZGNFJtVjVNRnh1YlRReFkxTTFaVFJRTkRsaU9XNDJlVXhDYWtKeVFtOXhjekpYUW1Gc01tVm5aR2hPU2xOelExZHZhVnBRT0M5YVZEaGxaMWgyYURkaU9URnFlbTlMTWx4dWNUSlFWVzFCTkVSblVVdENaMEZYVERKSmFuWkZTVEJQZVhneVV6RXhZMjR2WlROWFNtRlVVR2RPVUZSSE9UQXpWWEJoSzNGdWVtaFBTWGdyVFdGeGFGeHVVRVkwVjNOMVFYa3dRVzluUjBwM1owNUtZazQ0U0haTFZYTkZWSFpCTlhkNWVVNHpPVmhPTjNjd1kyaGhja1pNTXpkdmMxVXJWMDlCZWtScWJtcGpjMXh1UVhFMVR6ZEhVRWR0V0hWaU5rVkNVVUpRU21oS1VERjNlVFIyTDBzeFpraG5MMFkwTjNFMFpqUXdURUZLVDJ0aFdWSktSRFZJZWtKQmIwZENRVTVWYUZ4dWJrbENVRXB4Y1RSSlRYWlJObU5ET1djNFFpc3hlRmxFWldFNUwxbHJNWGNyVTIxUVIzWjNja1ZZZVROSFMzZzBTemRzUzNCaVVIbzNiVFJZTXpOemVGeHVjMFZWTHl0Wk1sWlJiWGRTWVRGNFVXMHZOVE55U3pkV01tdzFTbVl2UkRRd01HcFNiVFphWmxOQlQzWm5SRlJ5ZEZwdVZVZEtUWEo2T1VVM2RVNTNOMXh1YkdkVlNETktjbWwyZVVZdlpuaE5TVGh4YzNwUlluZFlVREFyZUc1NWNVRjRSVUZuWkhWTFFrRnZSMEZKVFN0UVUwNVpWME5hV0hoRWNGTklTVEU0WkZ4dWFrdHJiMEZpZHpKTmIzbDNVVWxzYTJWMVFXNHhaRmhHWVdReGVuTllVVWRrVkhKdFdIbDJOMDVRVUNzNFIxaENhMjVDVEdrelkzWjRWR2xzU2tsVGVWeHVkV05PY2tOTmFYRk9RVk51TDJSeE4yTlhSRVpWUVVKbmFsZ3hOa3BJTWtST1Jsb3ZiQzlWVmtZelRrUkJTbXBZUTNNeFdEZDVTVXA1V0VJMmIzZ3ZlbHh1WVZOc2NXeEpUVll6TlVSQ1JEZHhlRko1ZFV0eloyczlYRzR0TFMwdExVVk9SQ0JRVWtsV1FWUkZJRXRGV1MwdExTMHRYRzRpTEFvZ0lDSmpiR2xsYm5SZlpXMWhhV3dpT2lBaWNIVnNiQzF6WldOeVpYUXRkR1Z6ZEdsdVowQmlkV2xzWkMxamNtUXRkR1Z6ZEdsdVp5NXBZVzB1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlNJc0NpQWdJbU5zYVdWdWRGOXBaQ0k2SUNJeE1EYzVNelU0TmpJd016TXdNalV5TlRFek5USWlMQW9nSUNKaGRYUm9YM1Z5YVNJNklDSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMHZieTl2WVhWMGFESXZZWFYwYUNJc0NpQWdJblJ2YTJWdVgzVnlhU0k2SUNKb2RIUndjem92TDJGalkyOTFiblJ6TG1kdmIyZHNaUzVqYjIwdmJ5OXZZWFYwYURJdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOXdkV3hzTFhObFkzSmxkQzEwWlhOMGFXNW5KVFF3WW5WcGJHUXRZM0prTFhSbGMzUnBibWN1YVdGdExtZHpaWEoyYVdObFlXTmpiM1Z1ZEM1amIyMGlDbjA9In19 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: explicit + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: explicit +subjects: + - kind: ServiceAccount + name: explicit + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Pod +metadata: + name: explicit + annotations: + sidecar.istio.io/inject: "false" +spec: + serviceAccountName: explicit + containers: + - name: explicit + image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/explicit + restartPolicy: Never diff --git a/pkg/authn/k8schain/tests/implicit/main.go b/pkg/authn/k8schain/tests/implicit/main.go new file mode 100644 index 0000000..369240f --- /dev/null +++ b/pkg/authn/k8schain/tests/implicit/main.go @@ -0,0 +1,52 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + "os" + + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + if len(os.Args) != 2 { + log.Fatalf("expected usage: <command> <arg>, got: %v", os.Args) + } + + kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{}) + if err != nil { + log.Fatalf("k8schain.New() = %v", err) + } + + ref, err := name.NewDigest(os.Args[1]) + if err != nil { + log.Fatalf("NewDigest() = %v", err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc)) + if err != nil { + log.Fatalf("remote.Image() = %v", err) + } + + digest, err := img.Digest() + if err != nil { + log.Fatalf("Digest() = %v", err) + } + log.Printf("got digest: %v", digest) +} diff --git a/pkg/authn/k8schain/tests/implicit/test.yaml b/pkg/authn/k8schain/tests/implicit/test.yaml new file mode 100644 index 0000000..fba7e33 --- /dev/null +++ b/pkg/authn/k8schain/tests/implicit/test.yaml @@ -0,0 +1,47 @@ +# Copyright 2018 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: implicit + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: implicit +subjects: + - kind: ServiceAccount + name: implicit + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Pod +metadata: + name: implicit + annotations: + sidecar.istio.io/inject: "false" +spec: + serviceAccountName: implicit + containers: + - name: implicit + image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/implicit + args: + # This test assumes that the KO_DOCKER_REPO is private. + - github.com/google/go-containerregistry/pkg/authn/k8schain/tests/implicit + restartPolicy: Never diff --git a/pkg/authn/k8schain/tests/noauth/main.go b/pkg/authn/k8schain/tests/noauth/main.go new file mode 100644 index 0000000..cff69fa --- /dev/null +++ b/pkg/authn/k8schain/tests/noauth/main.go @@ -0,0 +1,47 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{}) + if err != nil { + log.Fatalf("k8schain.New() = %v", err) + } + + ref, err := name.ParseReference("ubuntu:latest") + if err != nil { + log.Fatalf("ParseReference() = %v", err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc)) + if err != nil { + log.Fatalf("remote.Image() = %v", err) + } + + digest, err := img.Digest() + if err != nil { + log.Fatalf("Digest() = %v", err) + } + log.Printf("got digest: %v", digest) +} diff --git a/pkg/authn/k8schain/tests/noauth/test.yaml b/pkg/authn/k8schain/tests/noauth/test.yaml new file mode 100644 index 0000000..a02b302 --- /dev/null +++ b/pkg/authn/k8schain/tests/noauth/test.yaml @@ -0,0 +1,44 @@ +# Copyright 2018 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: noauth + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: noauth +subjects: + - kind: ServiceAccount + name: noauth + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Pod +metadata: + name: noauth + annotations: + sidecar.istio.io/inject: "false" +spec: + serviceAccountName: noauth + containers: + - name: noauth + image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/noauth + restartPolicy: Never diff --git a/pkg/authn/k8schain/tests/serviceaccount/main.go b/pkg/authn/k8schain/tests/serviceaccount/main.go new file mode 100644 index 0000000..a895dc6 --- /dev/null +++ b/pkg/authn/k8schain/tests/serviceaccount/main.go @@ -0,0 +1,54 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "log" + + "github.com/google/go-containerregistry/pkg/authn/k8schain" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + ref, err := name.NewTag("gcr.io/build-crd-testing/secret-sauce:latest") + if err != nil { + log.Fatalf("NewTag() = %v", err) + } + + kc, err := k8schain.NewInCluster(context.Background(), k8schain.Options{ + Namespace: "serviceaccount-namespace", + ServiceAccountName: "serviceaccount", + // This is the name of the imagePullSecrets attached to this service account. + // ImagePullSecrets: []string{ + // "serviceaccount-secret", + // }, + }) + if err != nil { + log.Fatalf("k8schain.New() = %v", err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(kc)) + if err != nil { + log.Fatalf("remote.Image() = %v", err) + } + + digest, err := img.Digest() + if err != nil { + log.Fatalf("Digest() = %v", err) + } + log.Printf("got digest: %v", digest) +} diff --git a/pkg/authn/k8schain/tests/serviceaccount/test.yaml b/pkg/authn/k8schain/tests/serviceaccount/test.yaml new file mode 100644 index 0000000..f8fa089 --- /dev/null +++ b/pkg/authn/k8schain/tests/serviceaccount/test.yaml @@ -0,0 +1,67 @@ +# Copyright 2018 Google LLC All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: Namespace +metadata: + name: serviceaccount-namespace +--- +apiVersion: v1 +kind: Secret +metadata: + name: serviceaccount-secret + namespace: serviceaccount-namespace +type: kubernetes.io/dockercfg +data: + # This service account is JUST a storage reader on gcr.io/build-crd-testing + .dockercfg: eyJodHRwczovL2djci5pbyI6eyJ1c2VybmFtZSI6Il9qc29uX2tleSIsInBhc3N3b3JkIjoie1xuICBcInR5cGVcIjogXCJzZXJ2aWNlX2FjY291bnRcIixcbiAgXCJwcm9qZWN0X2lkXCI6IFwiYnVpbGQtY3JkLXRlc3RpbmdcIixcbiAgXCJwcml2YXRlX2tleV9pZFwiOiBcIjA1MDJhNDFhODEyZmI2NGNlNTZhNjhlYzU4MzJhYjBiYTExYzExZTZcIixcbiAgXCJwcml2YXRlX2tleVwiOiBcIi0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLVxcbk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRQzlYNEVZT0FSYnhRTThcXG5EMnhYY2FaVGsrZ1k4ZWp1OTh0THFDUXFUckdNVzlSZVQyeE9ZNUF5Z2FsUFArcDd5WEVja3dCRC9IaE0wZ2xJXFxuN01UTGRlZUtXcityQTFMd0haeVdGVzdIME9uZjd3bllIRUhMV1VtYzNCQ09SRUR0SFJaN1pyUEJmMUhUQUEvM1xcbk1uVzVsWkhTTjlvanpTU0Z3NkFWdTZqNmF4YkJJSUo3NTRMcmdLZUFZdXJ3ZklRMlJMVHUyMDFrMklxTFliaGJcXG4zbVNWRzVSK3RiS3oxQ3ZNNTNuSENiN0NmdVZlV3NyQThrazd4SHJyTFFLTW1JOXYyc2dSdWd5TUF6d3ovNnpOXFxuaDUvaU14eGdlcTVXOHhrVngzSjJuWThKSmRIYWYvVDZBR3NPTkVvNDNweGVpUVZqblJmL0tuMTBUQ2MyRXNJWVxcblM0OVVzWjdCQWdNQkFBRUNnZ0VBQXVwbGR1a0NRUXVENVUvZ2FtSHQ3R2dXM0FNVjE4ZXFuSG5DYTJqbGFoK1NcXG5BZVVHbmhnSmpOdkUrcE1GbFN2NXVmMnAySzRlZC9veEQ2K0NwOVpYRFJqZ3ZmdEl5cWpsemJ3dkZjZ3p3TnVEXFxueWdVa3VwN0hlY0RzRDhUdGVBb2JUL1Zwd3E2ektNckJ3Q3ZOa3Z5NmJWbG9FajV4M2JYc2F4ZTk1RE8veXB1NlxcbncwVzk3enh3d0RKWTZLUWNJV01qaHJHeHZ3WDduaVVDZU00bGVXQkR5R3R3MXplSm40aEVjNk4zYWpRYWNYS2NcXG4rNFFseGNpYW1ZcVFXYlBudHhXUWhoUXpjSFdMaTJsOWNGYlpENyt1SkxGNGlONnk4bVZOVTNLM0sxYlJZclNEXFxuUlVwM2FVVkJYbUZnK1ovMnB1VkwrbVUzajNMTFdZeUJPa2V2dU9tZGdRS0JnUURlM0dJUWt5V0lTMTRUZE1PU1xcbkJpS0JDRHk4aDk2ZWhMMEhrRGJ5T2tTdFBLZEY5cHVFeFp4aHk3b2pIQ0lNNUZWcnBSTjI1cDRzRXp3RmFjK3ZcXG5KSUZnRXZxN21YZm1YaVhJTmllUG9FUWFDbm54RHhXZ21yMEhVS0VtUzlvTWRnTGNHVStrQ1ZHTnN6N0FPdW0wXFxuS3FZM3MyMlE5bFE2N0ZPeXFpdThXRlE3UVFLQmdRRFppRmhURVprUEVjcVpqbndKcFRCNTZaV1A5S1RzbFpQN1xcbndVNGJ6aTZ5K21leWYzTUorNEwyU3lIYzNjcFNNYmp0Tk9aQ3Q0N2I5MDhGVW1MWFVHTmhjd3VaakVReEZleTBcXG5tNDFjUzVlNFA0OWI5bjZ5TEJqQnJCb3FzMldCYWwyZWdkaE5KU3NDV29pWlA4L1pUOGVnWHZoN2I5MWp6b0syXFxucTJQVW1BNERnUUtCZ0FXTDJJanZFSTBPeXgyUzExY24vZTNXSmFUUGdOUFRHOTAzVXBhK3FuemhPSXgrTWFxaFxcblBGNFdzdUF5MEFvZ0dKd2dOSmJOOEh2S1VzRVR2QTV3eXlOMzlYTjd3MGNoYXJGTDM3b3NVK1dPQXpEam5qY3NcXG5BcTVPN0dQR21YdWI2RUJRQlBKaEpQMXd5NHYvSzFmSGcvRjQ3cTRmNDBMQUpPa2FZUkpENUh6QkFvR0JBTlVoXFxubklCUEpxcTRJTXZRNmNDOWc4QisxeFlEZWE5L1lrMXcrU21QR3Z3ckVYeTNHS3g0SzdsS3BiUHo3bTRYMzNzeFxcbnNFVS8rWTJWUW13UmExeFFtLzUzcks3VjJsNUpmL0Q0MDBqUm02WmZTQU92Z0RUcnRablVHSk1yejlFN3VOdzdcXG5sZ1VIM0pyaXZ5Ri9meE1JOHFzelFid1hQMCt4bnlxQXhFQWdkdUtCQW9HQUlNK1BTTllXQ1pYeERwU0hJMThkXFxuaktrb0FidzJNb3l3UUlsa2V1QW4xZFhGYWQxenNYUUdkVHJtWHl2N05QUCs4R1hCa25CTGkzY3Z4VGlsSklTeVxcbnVjTnJDTWlxTkFTbi9kcTdjV0RGVUFCZ2pYMTZKSDJETkZaL2wvVVZGM05EQUpqWENzMVg3eUlKeVhCNm94L3pcXG5hU2xxbElNVjM1REJEN3F4Unl1S3Nnaz1cXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXFxuXCIsXG4gIFwiY2xpZW50X2VtYWlsXCI6IFwicHVsbC1zZWNyZXQtdGVzdGluZ0BidWlsZC1jcmQtdGVzdGluZy5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbVwiLFxuICBcImNsaWVudF9pZFwiOiBcIjEwNzkzNTg2MjAzMzAyNTI1MTM1MlwiLFxuICBcImF1dGhfdXJpXCI6IFwiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGhcIixcbiAgXCJ0b2tlbl91cmlcIjogXCJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW5cIixcbiAgXCJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmxcIjogXCJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9vYXV0aDIvdjEvY2VydHNcIixcbiAgXCJjbGllbnRfeDUwOV9jZXJ0X3VybFwiOiBcImh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL3JvYm90L3YxL21ldGFkYXRhL3g1MDkvcHVsbC1zZWNyZXQtdGVzdGluZyU0MGJ1aWxkLWNyZC10ZXN0aW5nLmlhbS5nc2VydmljZWFjY291bnQuY29tXCJcbn0iLCJlbWFpbCI6Im5vcmVwbHlAZ29vZ2xlLmNvbSIsImF1dGgiOiJYMnB6YjI1ZmEyVjVPbnNLSUNBaWRIbHdaU0k2SUNKelpYSjJhV05sWDJGalkyOTFiblFpTEFvZ0lDSndjbTlxWldOMFgybGtJam9nSW1KMWFXeGtMV055WkMxMFpYTjBhVzVuSWl3S0lDQWljSEpwZG1GMFpWOXJaWGxmYVdRaU9pQWlNRFV3TW1FME1XRTRNVEptWWpZMFkyVTFObUUyT0dWak5UZ3pNbUZpTUdKaE1URmpNVEZsTmlJc0NpQWdJbkJ5YVhaaGRHVmZhMlY1SWpvZ0lpMHRMUzB0UWtWSFNVNGdVRkpKVmtGVVJTQkxSVmt0TFMwdExWeHVUVWxKUlhaUlNVSkJSRUZPUW1kcmNXaHJhVWM1ZHpCQ1FWRkZSa0ZCVTBOQ1MyTjNaMmRUYWtGblJVRkJiMGxDUVZGRE9WZzBSVmxQUVZKaWVGRk5PRnh1UkRKNFdHTmhXbFJySzJkWk9HVnFkVGs0ZEV4eFExRnhWSEpIVFZjNVVtVlVNbmhQV1RWQmVXZGhiRkJRSzNBM2VWaEZZMnQzUWtRdlNHaE5NR2RzU1Z4dU4wMVVUR1JsWlV0WGNpdHlRVEZNZDBoYWVWZEdWemRJTUU5dVpqZDNibGxJUlVoTVYxVnRZek5DUTA5U1JVUjBTRkphTjFweVVFSm1NVWhVUVVFdk0xeHVUVzVYTld4YVNGTk9PVzlxZWxOVFJuYzJRVloxTm1vMllYaGlRa2xKU2pjMU5FeHlaMHRsUVZsMWNuZG1TVkV5VWt4VWRUSXdNV3N5U1hGTVdXSm9ZbHh1TTIxVFZrYzFVaXQwWWt0Nk1VTjJUVFV6YmtoRFlqZERablZXWlZkemNrRTRhMnMzZUVoeWNreFJTMDF0U1RsMk1uTm5VblZuZVUxQmVuZDZMelo2VGx4dWFEVXZhVTE0ZUdkbGNUVlhPSGhyVm5nelNqSnVXVGhLU21SSVlXWXZWRFpCUjNOUFRrVnZORE53ZUdWcFVWWnFibEptTDB0dU1UQlVRMk15UlhOSldWeHVVelE1VlhOYU4wSkJaMDFDUVVGRlEyZG5SVUZCZFhCc1pIVnJRMUZSZFVRMVZTOW5ZVzFJZERkSFoxY3pRVTFXTVRobGNXNUlia05oTW1wc1lXZ3JVMXh1UVdWVlIyNW9aMHBxVG5aRkszQk5SbXhUZGpWMVpqSndNa3MwWldRdmIzaEVOaXREY0RsYVdFUlNhbWQyWm5SSmVYRnFiSHBpZDNaR1kyZDZkMDUxUkZ4dWVXZFZhM1Z3TjBobFkwUnpSRGhVZEdWQmIySlVMMVp3ZDNFMmVrdE5ja0ozUTNaT2EzWjVObUpXYkc5RmFqVjRNMkpZYzJGNFpUazFSRTh2ZVhCMU5seHVkekJYT1RkNmVIZDNSRXBaTmt0UlkwbFhUV3BvY2tkNGRuZFlOMjVwVlVObFRUUnNaVmRDUkhsSGRIY3hlbVZLYmpSb1JXTTJUak5oYWxGaFkxaExZMXh1S3pSUmJIaGphV0Z0V1hGUlYySlFiblI0VjFGb2FGRjZZMGhYVEdreWJEbGpSbUphUkRjcmRVcE1SalJwVGpaNU9HMVdUbFV6U3pOTE1XSlNXWEpUUkZ4dVVsVndNMkZWVmtKWWJVWm5LMW92TW5CMVZrd3JiVlV6YWpOTVRGZFplVUpQYTJWMmRVOXRaR2RSUzBKblVVUmxNMGRKVVd0NVYwbFRNVFJVWkUxUFUxeHVRbWxMUWtORWVUaG9PVFpsYUV3d1NHdEVZbmxQYTFOMFVFdGtSamx3ZFVWNFduaG9lVGR2YWtoRFNVMDFSbFp5Y0ZKT01qVndOSE5GZW5kR1lXTXJkbHh1U2tsR1owVjJjVGR0V0dadFdHbFlTVTVwWlZCdlJWRmhRMjV1ZUVSNFYyZHRjakJJVlV0RmJWTTViMDFrWjB4alIxVXJhME5XUjA1emVqZEJUM1Z0TUZ4dVMzRlpNM015TWxFNWJGRTJOMFpQZVhGcGRUaFhSbEUzVVZGTFFtZFJSRnBwUm1oVVJWcHJVRVZqY1ZwcWJuZEtjRlJDTlRaYVYxQTVTMVJ6YkZwUU4xeHVkMVUwWW5wcE5ua3JiV1Y1WmpOTlNpczBUREpUZVVoak0yTndVMDFpYW5ST1QxcERkRFEzWWprd09FWlZiVXhZVlVkT2FHTjNkVnBxUlZGNFJtVjVNRnh1YlRReFkxTTFaVFJRTkRsaU9XNDJlVXhDYWtKeVFtOXhjekpYUW1Gc01tVm5aR2hPU2xOelExZHZhVnBRT0M5YVZEaGxaMWgyYURkaU9URnFlbTlMTWx4dWNUSlFWVzFCTkVSblVVdENaMEZYVERKSmFuWkZTVEJQZVhneVV6RXhZMjR2WlROWFNtRlVVR2RPVUZSSE9UQXpWWEJoSzNGdWVtaFBTWGdyVFdGeGFGeHVVRVkwVjNOMVFYa3dRVzluUjBwM1owNUtZazQ0U0haTFZYTkZWSFpCTlhkNWVVNHpPVmhPTjNjd1kyaGhja1pNTXpkdmMxVXJWMDlCZWtScWJtcGpjMXh1UVhFMVR6ZEhVRWR0V0hWaU5rVkNVVUpRU21oS1VERjNlVFIyTDBzeFpraG5MMFkwTjNFMFpqUXdURUZLVDJ0aFdWSktSRFZJZWtKQmIwZENRVTVWYUZ4dWJrbENVRXB4Y1RSSlRYWlJObU5ET1djNFFpc3hlRmxFWldFNUwxbHJNWGNyVTIxUVIzWjNja1ZZZVROSFMzZzBTemRzUzNCaVVIbzNiVFJZTXpOemVGeHVjMFZWTHl0Wk1sWlJiWGRTWVRGNFVXMHZOVE55U3pkV01tdzFTbVl2UkRRd01HcFNiVFphWmxOQlQzWm5SRlJ5ZEZwdVZVZEtUWEo2T1VVM2RVNTNOMXh1YkdkVlNETktjbWwyZVVZdlpuaE5TVGh4YzNwUlluZFlVREFyZUc1NWNVRjRSVUZuWkhWTFFrRnZSMEZKVFN0UVUwNVpWME5hV0hoRWNGTklTVEU0WkZ4dWFrdHJiMEZpZHpKTmIzbDNVVWxzYTJWMVFXNHhaRmhHWVdReGVuTllVVWRrVkhKdFdIbDJOMDVRVUNzNFIxaENhMjVDVEdrelkzWjRWR2xzU2tsVGVWeHVkV05PY2tOTmFYRk9RVk51TDJSeE4yTlhSRVpWUVVKbmFsZ3hOa3BJTWtST1Jsb3ZiQzlWVmtZelRrUkJTbXBZUTNNeFdEZDVTVXA1V0VJMmIzZ3ZlbHh1WVZOc2NXeEpUVll6TlVSQ1JEZHhlRko1ZFV0eloyczlYRzR0TFMwdExVVk9SQ0JRVWtsV1FWUkZJRXRGV1MwdExTMHRYRzRpTEFvZ0lDSmpiR2xsYm5SZlpXMWhhV3dpT2lBaWNIVnNiQzF6WldOeVpYUXRkR1Z6ZEdsdVowQmlkV2xzWkMxamNtUXRkR1Z6ZEdsdVp5NXBZVzB1WjNObGNuWnBZMlZoWTJOdmRXNTBMbU52YlNJc0NpQWdJbU5zYVdWdWRGOXBaQ0k2SUNJeE1EYzVNelU0TmpJd016TXdNalV5TlRFek5USWlMQW9nSUNKaGRYUm9YM1Z5YVNJNklDSm9kSFJ3Y3pvdkwyRmpZMjkxYm5SekxtZHZiMmRzWlM1amIyMHZieTl2WVhWMGFESXZZWFYwYUNJc0NpQWdJblJ2YTJWdVgzVnlhU0k2SUNKb2RIUndjem92TDJGalkyOTFiblJ6TG1kdmIyZHNaUzVqYjIwdmJ5OXZZWFYwYURJdmRHOXJaVzRpTEFvZ0lDSmhkWFJvWDNCeWIzWnBaR1Z5WDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2YjJGMWRHZ3lMM1l4TDJObGNuUnpJaXdLSUNBaVkyeHBaVzUwWDNnMU1EbGZZMlZ5ZEY5MWNtd2lPaUFpYUhSMGNITTZMeTkzZDNjdVoyOXZaMnhsWVhCcGN5NWpiMjB2Y205aWIzUXZkakV2YldWMFlXUmhkR0V2ZURVd09TOXdkV3hzTFhObFkzSmxkQzEwWlhOMGFXNW5KVFF3WW5WcGJHUXRZM0prTFhSbGMzUnBibWN1YVdGdExtZHpaWEoyYVdObFlXTmpiM1Z1ZEM1amIyMGlDbjA9In19 +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: serviceaccount + namespace: serviceaccount-namespace +imagePullSecrets: +- name: serviceaccount-secret +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: serviceaccount + namespace: default +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: serviceaccount +subjects: + - kind: ServiceAccount + name: serviceaccount + namespace: default +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: v1 +kind: Pod +metadata: + name: serviceaccount + annotations: + sidecar.istio.io/inject: "false" +spec: + serviceAccountName: serviceaccount + containers: + - name: serviceaccount + image: github.com/google/go-containerregistry/pkg/authn/k8schain/tests/serviceaccount + restartPolicy: Never diff --git a/pkg/authn/keychain.go b/pkg/authn/keychain.go new file mode 100644 index 0000000..a4a88b3 --- /dev/null +++ b/pkg/authn/keychain.go @@ -0,0 +1,180 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "os" + "path/filepath" + "sync" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/cli/config/types" + "github.com/google/go-containerregistry/pkg/name" + "github.com/mitchellh/go-homedir" +) + +// Resource represents a registry or repository that can be authenticated against. +type Resource interface { + // String returns the full string representation of the target, e.g. + // gcr.io/my-project or just gcr.io. + String() string + + // RegistryStr returns just the registry portion of the target, e.g. for + // gcr.io/my-project, this should just return gcr.io. This is needed to + // pull out an appropriate hostname. + RegistryStr() string +} + +// Keychain is an interface for resolving an image reference to a credential. +type Keychain interface { + // Resolve looks up the most appropriate credential for the specified target. + Resolve(Resource) (Authenticator, error) +} + +// defaultKeychain implements Keychain with the semantics of the standard Docker +// credential keychain. +type defaultKeychain struct { + mu sync.Mutex +} + +var ( + // DefaultKeychain implements Keychain by interpreting the docker config file. + DefaultKeychain Keychain = &defaultKeychain{} +) + +const ( + // DefaultAuthKey is the key used for dockerhub in config files, which + // is hardcoded for historical reasons. + DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/" +) + +// Resolve implements Keychain. +func (dk *defaultKeychain) Resolve(target Resource) (Authenticator, error) { + dk.mu.Lock() + defer dk.mu.Unlock() + + // Podman users may have their container registry auth configured in a + // different location, that Docker packages aren't aware of. + // If the Docker config file isn't found, we'll fallback to look where + // Podman configures it, and parse that as a Docker auth config instead. + + // First, check $HOME/.docker/config.json + foundDockerConfig := false + home, err := homedir.Dir() + if err == nil { + foundDockerConfig = fileExists(filepath.Join(home, ".docker/config.json")) + } + // If $HOME/.docker/config.json isn't found, check $DOCKER_CONFIG (if set) + if !foundDockerConfig && os.Getenv("DOCKER_CONFIG") != "" { + foundDockerConfig = fileExists(filepath.Join(os.Getenv("DOCKER_CONFIG"), "config.json")) + } + // If either of those locations are found, load it using Docker's + // config.Load, which may fail if the config can't be parsed. + // + // If neither was found, look for Podman's auth at + // $XDG_RUNTIME_DIR/containers/auth.json and attempt to load it as a + // Docker config. + // + // If neither are found, fallback to Anonymous. + var cf *configfile.ConfigFile + if foundDockerConfig { + cf, err = config.Load(os.Getenv("DOCKER_CONFIG")) + if err != nil { + return nil, err + } + } else { + f, err := os.Open(filepath.Join(os.Getenv("XDG_RUNTIME_DIR"), "containers/auth.json")) + if err != nil { + return Anonymous, nil + } + defer f.Close() + cf, err = config.LoadFromReader(f) + if err != nil { + return nil, err + } + } + + // See: + // https://github.com/google/ko/issues/90 + // https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404 + var cfg, empty types.AuthConfig + for _, key := range []string{ + target.String(), + target.RegistryStr(), + } { + if key == name.DefaultRegistry { + key = DefaultAuthKey + } + + cfg, err = cf.GetAuthConfig(key) + if err != nil { + return nil, err + } + // cf.GetAuthConfig automatically sets the ServerAddress attribute. Since + // we don't make use of it, clear the value for a proper "is-empty" test. + // See: https://github.com/google/go-containerregistry/issues/1510 + cfg.ServerAddress = "" + if cfg != empty { + break + } + } + if cfg == empty { + return Anonymous, nil + } + + return FromConfig(AuthConfig{ + Username: cfg.Username, + Password: cfg.Password, + Auth: cfg.Auth, + IdentityToken: cfg.IdentityToken, + RegistryToken: cfg.RegistryToken, + }), nil +} + +// fileExists returns true if the given path exists and is not a directory. +func fileExists(path string) bool { + fi, err := os.Stat(path) + return err == nil && !fi.IsDir() +} + +// Helper is a subset of the Docker credential helper credentials.Helper +// interface used by NewKeychainFromHelper. +// +// See: +// https://pkg.go.dev/github.com/docker/docker-credential-helpers/credentials#Helper +type Helper interface { + Get(serverURL string) (string, string, error) +} + +// NewKeychainFromHelper returns a Keychain based on a Docker credential helper +// implementation that can Get username and password credentials for a given +// server URL. +func NewKeychainFromHelper(h Helper) Keychain { return wrapper{h} } + +type wrapper struct{ h Helper } + +func (w wrapper) Resolve(r Resource) (Authenticator, error) { + u, p, err := w.h.Get(r.RegistryStr()) + if err != nil { + return Anonymous, nil + } + // If the secret being stored is an identity token, the Username should be set to <token> + // ref: https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol + if u == "<token>" { + return FromConfig(AuthConfig{Username: u, IdentityToken: p}), nil + } + return FromConfig(AuthConfig{Username: u, Password: p}), nil +} diff --git a/pkg/authn/keychain_test.go b/pkg/authn/keychain_test.go new file mode 100644 index 0000000..9dfbad1 --- /dev/null +++ b/pkg/authn/keychain_test.go @@ -0,0 +1,392 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "encoding/base64" + "errors" + "fmt" + "log" + "os" + "path" + "path/filepath" + "reflect" + "testing" + + "github.com/google/go-containerregistry/pkg/name" +) + +var ( + fresh = 0 + testRegistry, _ = name.NewRegistry("test.io", name.WeakValidation) + testRepo, _ = name.NewRepository("test.io/my-repo", name.WeakValidation) + defaultRegistry, _ = name.NewRegistry(name.DefaultRegistry, name.WeakValidation) +) + +func TestMain(m *testing.M) { + // Set $HOME to a temp empty dir, to ensure $HOME/.docker/config.json + // isn't unexpectedly found. + tmp, err := os.MkdirTemp("", "keychain_test_home") + if err != nil { + log.Fatal(err) + } + os.Setenv("HOME", tmp) + os.Exit(func() int { + defer os.RemoveAll(tmp) + return m.Run() + }()) +} + +// setupConfigDir sets up an isolated configDir() for this test. +func setupConfigDir(t *testing.T) string { + tmpdir := os.Getenv("TEST_TMPDIR") + if tmpdir == "" { + tmpdir = t.TempDir() + } + + fresh++ + p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh)) + t.Logf("DOCKER_CONFIG=%s", p) + t.Setenv("DOCKER_CONFIG", p) + if err := os.Mkdir(p, 0777); err != nil { + t.Fatalf("mkdir %q: %v", p, err) + } + return p +} + +func setupConfigFile(t *testing.T, content string) string { + cd := setupConfigDir(t) + p := filepath.Join(cd, "config.json") + if err := os.WriteFile(p, []byte(content), 0600); err != nil { + t.Fatalf("write %q: %v", p, err) + } + + // return the config dir so we can clean up + return cd +} + +func TestNoConfig(t *testing.T) { + cd := setupConfigDir(t) + defer os.RemoveAll(filepath.Dir(cd)) + + auth, err := DefaultKeychain.Resolve(testRegistry) + if err != nil { + t.Fatalf("Resolve() = %v", err) + } + + if auth != Anonymous { + t.Errorf("expected Anonymous, got %v", auth) + } +} + +func TestPodmanConfig(t *testing.T) { + tmpdir := os.Getenv("TEST_TMPDIR") + if tmpdir == "" { + tmpdir = t.TempDir() + } + fresh++ + p := filepath.Join(tmpdir, fmt.Sprintf("%d", fresh)) + t.Setenv("XDG_RUNTIME_DIR", p) + os.Unsetenv("DOCKER_CONFIG") + if err := os.MkdirAll(filepath.Join(p, "containers"), 0777); err != nil { + t.Fatalf("mkdir %s/containers: %v", p, err) + } + cfg := filepath.Join(p, "containers/auth.json") + content := fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")) + if err := os.WriteFile(cfg, []byte(content), 0600); err != nil { + t.Fatalf("write %q: %v", cfg, err) + } + + // At first, $DOCKER_CONFIG is unset and $HOME/.docker/config.json isn't + // found, but Podman auth is configured. This should return Podman's + // auth. + auth, err := DefaultKeychain.Resolve(testRegistry) + if err != nil { + t.Fatalf("Resolve() = %v", err) + } + got, err := auth.Authorization() + if err != nil { + t.Fatal(err) + } + want := &AuthConfig{ + Username: "foo", + Password: "bar", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } + + // Now, configure $HOME/.docker/config.json, which should override + // Podman auth and be used. + if err := os.MkdirAll(filepath.Join(os.Getenv("HOME"), ".docker"), 0777); err != nil { + t.Fatalf("mkdir $HOME/.docker: %v", err) + } + cfg = filepath.Join(os.Getenv("HOME"), ".docker/config.json") + content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("home-foo", "home-bar")) + if err := os.WriteFile(cfg, []byte(content), 0600); err != nil { + t.Fatalf("write %q: %v", cfg, err) + } + defer func() { os.Remove(cfg) }() + auth, err = DefaultKeychain.Resolve(testRegistry) + if err != nil { + t.Fatalf("Resolve() = %v", err) + } + got, err = auth.Authorization() + if err != nil { + t.Fatal(err) + } + want = &AuthConfig{ + Username: "home-foo", + Password: "home-bar", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } + + // Then, configure DOCKER_CONFIG with a valid config file with different + // auth configured. + // This demonstrates that DOCKER_CONFIG is preferred over Podman auth + // and $HOME/.docker/config.json. + content = fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("another-foo", "another-bar")) + cd := setupConfigFile(t, content) + defer os.RemoveAll(filepath.Dir(cd)) + + auth, err = DefaultKeychain.Resolve(testRegistry) + if err != nil { + t.Fatalf("Resolve() = %v", err) + } + got, err = auth.Authorization() + if err != nil { + t.Fatal(err) + } + want = &AuthConfig{ + Username: "another-foo", + Password: "another-bar", + } + if !reflect.DeepEqual(got, want) { + t.Errorf("got %+v, want %+v", got, want) + } +} + +func encode(user, pass string) string { + delimited := fmt.Sprintf("%s:%s", user, pass) + return base64.StdEncoding.EncodeToString([]byte(delimited)) +} + +func TestVariousPaths(t *testing.T) { + tests := []struct { + desc string + content string + wantErr bool + target Resource + cfg *AuthConfig + anonymous bool + }{{ + desc: "invalid config file", + target: testRegistry, + content: `}{`, + wantErr: true, + }, { + desc: "creds store does not exist", + target: testRegistry, + content: `{"credsStore":"#definitely-does-not-exist"}`, + wantErr: true, + }, { + desc: "valid config file", + target: testRegistry, + content: fmt.Sprintf(`{"auths": {"test.io": {"auth": %q}}}`, encode("foo", "bar")), + cfg: &AuthConfig{ + Username: "foo", + Password: "bar", + }, + }, { + desc: "valid config file; default registry", + target: defaultRegistry, + content: fmt.Sprintf(`{"auths": {"%s": {"auth": %q}}}`, DefaultAuthKey, encode("foo", "bar")), + cfg: &AuthConfig{ + Username: "foo", + Password: "bar", + }, + }, { + desc: "valid config file; matches registry w/ v1", + target: testRegistry, + content: fmt.Sprintf(`{ + "auths": { + "http://test.io/v1/": {"auth": %q} + } + }`, encode("baz", "quux")), + cfg: &AuthConfig{ + Username: "baz", + Password: "quux", + }, + }, { + desc: "valid config file; matches registry w/ v2", + target: testRegistry, + content: fmt.Sprintf(`{ + "auths": { + "http://test.io/v2/": {"auth": %q} + } + }`, encode("baz", "quux")), + cfg: &AuthConfig{ + Username: "baz", + Password: "quux", + }, + }, { + desc: "valid config file; matches repo", + target: testRepo, + content: fmt.Sprintf(`{ + "auths": { + "test.io/my-repo": {"auth": %q}, + "test.io/another-repo": {"auth": %q}, + "test.io": {"auth": %q} + } +}`, encode("foo", "bar"), encode("bar", "baz"), encode("baz", "quux")), + cfg: &AuthConfig{ + Username: "foo", + Password: "bar", + }, + }, { + desc: "ignore unrelated repo", + target: testRepo, + content: fmt.Sprintf(`{ + "auths": { + "test.io/another-repo": {"auth": %q}, + "test.io": {} + } +}`, encode("bar", "baz")), + cfg: &AuthConfig{}, + anonymous: true, + }} + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + cd := setupConfigFile(t, test.content) + // For some reason, these tempdirs don't get cleaned up. + defer os.RemoveAll(filepath.Dir(cd)) + + auth, err := DefaultKeychain.Resolve(test.target) + if test.wantErr { + if err == nil { + t.Fatal("wanted err, got nil") + } else if err != nil { + // success + return + } + } + if err != nil { + t.Fatalf("wanted nil, got err: %v", err) + } + cfg, err := auth.Authorization() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(cfg, test.cfg) { + t.Errorf("got %+v, want %+v", cfg, test.cfg) + } + + if test.anonymous != (auth == Anonymous) { + t.Fatalf("unexpected anonymous authenticator") + } + }) + } +} + +type helper struct { + u, p string + err error +} + +func (h helper) Get(serverURL string) (string, string, error) { + if serverURL != "example.com" { + return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL) + } + return h.u, h.p, h.err +} + +func TestNewKeychainFromHelper(t *testing.T) { + var repo = name.MustParseReference("example.com/my/repo").Context() + + t.Run("success", func(t *testing.T) { + kc := NewKeychainFromHelper(helper{"username", "password", nil}) + auth, err := kc.Resolve(repo) + if err != nil { + t.Fatalf("Resolve(%q): %v", repo, err) + } + cfg, err := auth.Authorization() + if err != nil { + t.Fatalf("Authorization: %v", err) + } + if got, want := cfg.Username, "username"; got != want { + t.Errorf("Username: got %q, want %q", got, want) + } + if got, want := cfg.IdentityToken, ""; got != want { + t.Errorf("IdentityToken: got %q, want %q", got, want) + } + if got, want := cfg.Password, "password"; got != want { + t.Errorf("Password: got %q, want %q", got, want) + } + }) + + t.Run("success; identity token", func(t *testing.T) { + kc := NewKeychainFromHelper(helper{"<token>", "idtoken", nil}) + auth, err := kc.Resolve(repo) + if err != nil { + t.Fatalf("Resolve(%q): %v", repo, err) + } + cfg, err := auth.Authorization() + if err != nil { + t.Fatalf("Authorization: %v", err) + } + if got, want := cfg.Username, "<token>"; got != want { + t.Errorf("Username: got %q, want %q", got, want) + } + if got, want := cfg.IdentityToken, "idtoken"; got != want { + t.Errorf("IdentityToken: got %q, want %q", got, want) + } + if got, want := cfg.Password, ""; got != want { + t.Errorf("Password: got %q, want %q", got, want) + } + }) + + t.Run("failure", func(t *testing.T) { + kc := NewKeychainFromHelper(helper{"", "", errors.New("oh no bad")}) + auth, err := kc.Resolve(repo) + if err != nil { + t.Fatalf("Resolve(%q): %v", repo, err) + } + if auth != Anonymous { + t.Errorf("Resolve: got %v, want %v", auth, Anonymous) + } + }) +} + +func TestConfigFileIsADir(t *testing.T) { + tmpdir := setupConfigDir(t) + // Create "config.json" as a directory, not a file to simulate optional + // secrets in Kubernetes. + err := os.Mkdir(path.Join(tmpdir, "config.json"), 0777) + if err != nil { + t.Fatal(err) + } + + auth, err := DefaultKeychain.Resolve(testRegistry) + if err != nil { + t.Fatalf("Resolve() = %v", err) + } + if auth != Anonymous { + t.Errorf("expected Anonymous, got %v", auth) + } +} diff --git a/pkg/authn/kubernetes/go.mod b/pkg/authn/kubernetes/go.mod new file mode 100644 index 0000000..ba2f45c --- /dev/null +++ b/pkg/authn/kubernetes/go.mod @@ -0,0 +1,59 @@ +module github.com/google/go-containerregistry/pkg/authn/kubernetes + +go 1.18 + +replace github.com/google/go-containerregistry => ../../../ + +require ( + github.com/google/go-cmp v0.5.9 + github.com/google/go-containerregistry v0.13.0 + k8s.io/api v0.26.2 + k8s.io/apimachinery v0.26.2 + k8s.io/client-go v0.26.2 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v23.0.1+incompatible // indirect + github.com/docker/docker v23.0.1+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.10.2 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.6.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.29.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.1.0 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect + k8s.io/utils v0.0.0-20230308161112-d77c459e9343 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) diff --git a/pkg/authn/kubernetes/go.sum b/pkg/authn/kubernetes/go.sum new file mode 100644 index 0000000..df8143e --- /dev/null +++ b/pkg/authn/kubernetes/go.sum @@ -0,0 +1,276 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v23.0.1+incompatible h1:LRyWITpGzl2C9e9uGxzisptnxAn1zfZKXy13Ul2Q5oM= +github.com/docker/cli v23.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= +github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= +github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= +github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.4.0 h1:+Ig9nvqgS5OBSACXNk15PLdp0U9XPYROt9CFzVdFGIs= +github.com/onsi/gomega v1.23.0 h1:/oxKu9c2HVap+F3PfKort2Hw5DEU+HGlW8n+tguWsys= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.29.0 h1:44S3JjaKmLEE4YIkjzexaP+NzZsudE3Zin5Njn/pYX0= +google.golang.org/protobuf v1.29.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= +gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.26.2 h1:dM3cinp3PGB6asOySalOZxEG4CZ0IAdJsrYZXE/ovGQ= +k8s.io/api v0.26.2/go.mod h1:1kjMQsFE+QHPfskEcVNgL3+Hp88B80uj0QtSOlj8itU= +k8s.io/apimachinery v0.26.2 h1:da1u3D5wfR5u2RpLhE/ZtZS2P7QvDgLZTi9wrNZl/tQ= +k8s.io/apimachinery v0.26.2/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= +k8s.io/client-go v0.26.2 h1:s1WkVujHX3kTp4Zn4yGNFK+dlDXy1bAAkIl+cFAiuYI= +k8s.io/client-go v0.26.2/go.mod h1:u5EjOuSyBa09yqqyY7m3abZeovO/7D/WehVVlZ2qcqU= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= +k8s.io/utils v0.0.0-20230308161112-d77c459e9343 h1:m7tbIjXGcGIAtpmQr7/NAi7RsWoW3E7Zcm4jI1HicTc= +k8s.io/utils v0.0.0-20230308161112-d77c459e9343/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/authn/kubernetes/keychain.go b/pkg/authn/kubernetes/keychain.go new file mode 100644 index 0000000..368d829 --- /dev/null +++ b/pkg/authn/kubernetes/keychain.go @@ -0,0 +1,331 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/url" + "path/filepath" + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +const ( + // NoServiceAccount is a constant that can be passed via ServiceAccountName + // to tell the keychain that looking up the service account is unnecessary. + // This value cannot collide with an actual service account name because + // service accounts do not allow spaces. + NoServiceAccount = "no service account" +) + +// Options holds configuration data for guiding credential resolution. +type Options struct { + // Namespace holds the namespace inside of which we are resolving service + // account and pull secret references to access the image. + // If empty, "default" is assumed. + Namespace string + + // ServiceAccountName holds the serviceaccount (within Namespace) as which a + // Pod might access the image. Service accounts may have image pull secrets + // attached, so we lookup the service account to complete the keychain. + // If empty, "default" is assumed. To avoid a service account lookup, pass + // NoServiceAccount explicitly. + ServiceAccountName string + + // ImagePullSecrets holds the names of the Kubernetes secrets (scoped to + // Namespace) containing credential data to use for the image pull. + ImagePullSecrets []string + + // UseMountSecrets determines whether or not mount secrets in the ServiceAccount + // should be considered. Mount secrets are those listed under the `.secrets` + // attribute of the ServiceAccount resource. Ignored if ServiceAccountName is set + // to NoServiceAccount. + UseMountSecrets bool +} + +// New returns a new authn.Keychain suitable for resolving image references as +// scoped by the provided Options. It speaks to Kubernetes through the provided +// client interface. +func New(ctx context.Context, client kubernetes.Interface, opt Options) (authn.Keychain, error) { + if opt.Namespace == "" { + opt.Namespace = "default" + } + if opt.ServiceAccountName == "" { + opt.ServiceAccountName = "default" + } + + // Implement a Kubernetes-style authentication keychain. + // This needs to support roughly the following kinds of authentication: + // 1) The explicit authentication from imagePullSecrets on Pod + // 2) The semi-implicit authentication where imagePullSecrets are on the + // Pod's service account. + + // First, fetch all of the explicitly declared pull secrets + var pullSecrets []corev1.Secret + for _, name := range opt.ImagePullSecrets { + ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, name, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, name) + continue + } else if err != nil { + return nil, err + } + pullSecrets = append(pullSecrets, *ps) + } + + // Second, fetch all of the pull secrets attached to our service account, + // unless the user has explicitly specified that no service account lookup + // is desired. + if opt.ServiceAccountName != NoServiceAccount { + sa, err := client.CoreV1().ServiceAccounts(opt.Namespace).Get(ctx, opt.ServiceAccountName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + logs.Warn.Printf("serviceaccount %s/%s not found; ignoring", opt.Namespace, opt.ServiceAccountName) + } else if err != nil { + return nil, err + } + if sa != nil { + for _, localObj := range sa.ImagePullSecrets { + ps, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, localObj.Name, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, localObj.Name) + continue + } else if err != nil { + return nil, err + } + pullSecrets = append(pullSecrets, *ps) + } + + if opt.UseMountSecrets { + for _, obj := range sa.Secrets { + s, err := client.CoreV1().Secrets(opt.Namespace).Get(ctx, obj.Name, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, obj.Name) + continue + } else if err != nil { + return nil, err + } + pullSecrets = append(pullSecrets, *s) + } + } + } + } + + return NewFromPullSecrets(ctx, pullSecrets) +} + +// NewInCluster returns a new authn.Keychain suitable for resolving image references as +// scoped by the provided Options, constructing a kubernetes.Interface based on in-cluster +// authentication. +func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) { + clusterConfig, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + + client, err := kubernetes.NewForConfig(clusterConfig) + if err != nil { + return nil, err + } + return New(ctx, client, opt) +} + +type dockerConfigJSON struct { + Auths map[string]authn.AuthConfig +} + +// NewFromPullSecrets returns a new authn.Keychain suitable for resolving image references as +// scoped by the pull secrets. +func NewFromPullSecrets(ctx context.Context, secrets []corev1.Secret) (authn.Keychain, error) { + keyring := &keyring{ + index: make([]string, 0), + creds: make(map[string][]authn.AuthConfig), + } + + var cfg dockerConfigJSON + + // From: https://github.com/kubernetes/kubernetes/blob/0dcafb1f37ee522be3c045753623138e5b907001/pkg/credentialprovider/keyring.go + for _, secret := range secrets { + if b, exists := secret.Data[corev1.DockerConfigJsonKey]; secret.Type == corev1.SecretTypeDockerConfigJson && exists && len(b) > 0 { + if err := json.Unmarshal(b, &cfg); err != nil { + return nil, err + } + } + if b, exists := secret.Data[corev1.DockerConfigKey]; secret.Type == corev1.SecretTypeDockercfg && exists && len(b) > 0 { + if err := json.Unmarshal(b, &cfg.Auths); err != nil { + return nil, err + } + } + + for registry, v := range cfg.Auths { + value := registry + if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") { + value = "https://" + value + } + parsed, err := url.Parse(value) + if err != nil { + return nil, fmt.Errorf("Entry %q in dockercfg invalid (%w)", value, err) + } + + // The docker client allows exact matches: + // foo.bar.com/namespace + // Or hostname matches: + // foo.bar.com + // It also considers /v2/ and /v1/ equivalent to the hostname + // See ResolveAuthConfig in docker/registry/auth.go. + effectivePath := parsed.Path + if strings.HasPrefix(effectivePath, "/v2/") || strings.HasPrefix(effectivePath, "/v1/") { + effectivePath = effectivePath[3:] + } + var key string + if (len(effectivePath) > 0) && (effectivePath != "/") { + key = parsed.Host + effectivePath + } else { + key = parsed.Host + } + + if _, ok := keyring.creds[key]; !ok { + keyring.index = append(keyring.index, key) + } + + keyring.creds[key] = append(keyring.creds[key], v) + + } + + // We reverse sort in to give more specific (aka longer) keys priority + // when matching for creds + sort.Sort(sort.Reverse(sort.StringSlice(keyring.index))) + } + return keyring, nil +} + +type keyring struct { + index []string + creds map[string][]authn.AuthConfig +} + +func (keyring *keyring) Resolve(target authn.Resource) (authn.Authenticator, error) { + image := target.String() + auths := []authn.AuthConfig{} + + for _, k := range keyring.index { + // both k and image are schemeless URLs because even though schemes are allowed + // in the credential configurations, we remove them when constructing the keyring + if matched, _ := urlsMatchStr(k, image); matched { + auths = append(auths, keyring.creds[k]...) + } + } + + if len(auths) == 0 { + return authn.Anonymous, nil + } + + return toAuthenticator(auths) +} + +// urlsMatchStr is wrapper for URLsMatch, operating on strings instead of URLs. +func urlsMatchStr(glob string, target string) (bool, error) { + globURL, err := parseSchemelessURL(glob) + if err != nil { + return false, err + } + targetURL, err := parseSchemelessURL(target) + if err != nil { + return false, err + } + return urlsMatch(globURL, targetURL) +} + +// parseSchemelessURL parses a schemeless url and returns a url.URL +// url.Parse require a scheme, but ours don't have schemes. Adding a +// scheme to make url.Parse happy, then clear out the resulting scheme. +func parseSchemelessURL(schemelessURL string) (*url.URL, error) { + parsed, err := url.Parse("https://" + schemelessURL) + if err != nil { + return nil, err + } + // clear out the resulting scheme + parsed.Scheme = "" + return parsed, nil +} + +// splitURL splits the host name into parts, as well as the port +func splitURL(url *url.URL) (parts []string, port string) { + host, port, err := net.SplitHostPort(url.Host) + if err != nil { + // could not parse port + host, port = url.Host, "" + } + return strings.Split(host, "."), port +} + +// urlsMatch checks whether the given target url matches the glob url, which may have +// glob wild cards in the host name. +// +// Examples: +// +// globURL=*.docker.io, targetURL=blah.docker.io => match +// globURL=*.docker.io, targetURL=not.right.io => no match +// +// Note that we don't support wildcards in ports and paths yet. +func urlsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) { + globURLParts, globPort := splitURL(globURL) + targetURLParts, targetPort := splitURL(targetURL) + if globPort != targetPort { + // port doesn't match + return false, nil + } + if len(globURLParts) != len(targetURLParts) { + // host name does not have the same number of parts + return false, nil + } + if !strings.HasPrefix(targetURL.Path, globURL.Path) { + // the path of the credential must be a prefix + return false, nil + } + for k, globURLPart := range globURLParts { + targetURLPart := targetURLParts[k] + matched, err := filepath.Match(globURLPart, targetURLPart) + if err != nil { + return false, err + } + if !matched { + // glob mismatch for some part + return false, nil + } + } + // everything matches + return true, nil +} + +func toAuthenticator(configs []authn.AuthConfig) (authn.Authenticator, error) { + cfg := configs[0] + + if cfg.Auth != "" { + cfg.Auth = "" + } + + return authn.FromConfig(cfg), nil +} diff --git a/pkg/authn/kubernetes/keychain_test.go b/pkg/authn/kubernetes/keychain_test.go new file mode 100644 index 0000000..e015771 --- /dev/null +++ b/pkg/authn/kubernetes/keychain_test.go @@ -0,0 +1,586 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package kubernetes + +import ( + "context" + "crypto/md5" + "encoding/base64" + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + fakeclient "k8s.io/client-go/kubernetes/fake" +) + +var dockerSecretTypes = []secretType{ + dockerConfigJSONSecretType, + dockerCfgSecretType, +} + +type secretType struct { + name corev1.SecretType + key string + marshal func(t *testing.T, registry string, auth authn.AuthConfig) []byte +} + +func (s *secretType) Create(t *testing.T, namespace, name string, registry string, auth authn.AuthConfig) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: s.name, + Data: map[string][]byte{ + s.key: s.marshal(t, registry, auth), + }, + } +} + +var dockerConfigJSONSecretType = secretType{ + name: corev1.SecretTypeDockerConfigJson, + key: corev1.DockerConfigJsonKey, + marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte { + return toJSON(t, dockerConfigJSON{ + Auths: map[string]authn.AuthConfig{target: auth}, + }) + }, +} + +var dockerCfgSecretType = secretType{ + name: corev1.SecretTypeDockercfg, + key: corev1.DockerConfigKey, + marshal: func(t *testing.T, target string, auth authn.AuthConfig) []byte { + return toJSON(t, map[string]authn.AuthConfig{target: auth}) + }, +} + +func TestAnonymousFallback(t *testing.T) { + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "default", + }, + }) + + kc, err := New(context.Background(), client, Options{}) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestAnonymousFallbackNoServiceAccount(t *testing.T) { + kc, err := New(context.Background(), nil, Options{ + ServiceAccountName: NoServiceAccount, + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestSecretNotFound(t *testing.T) { + client := fakeclient.NewSimpleClientset() + + kc, err := New(context.Background(), client, Options{ + ServiceAccountName: NoServiceAccount, + ImagePullSecrets: []string{"not-found"}, + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestServiceAccountNotFound(t *testing.T) { + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "default", + }, + }) + kc, err := New(context.Background(), client, Options{ + ServiceAccountName: "not-found", + }) + if err != nil { + t.Errorf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.Anonymous) +} + +func TestImagePullSecretAttachedServiceAccount(t *testing.T) { + username, password := "foo", "bar" + client := fakeclient.NewSimpleClientset(&corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcacct", + Namespace: "ns", + }, + ImagePullSecrets: []corev1.LocalObjectReference{{ + Name: "secret", + }}, + }, + dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: username, + Password: password, + }), + ) + + kc, err := New(context.Background(), client, Options{ + Namespace: "ns", + ServiceAccountName: "svcacct", + }) + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), + &authn.Basic{Username: username, Password: password}) +} + +func TestSecretAttachedServiceAccount(t *testing.T) { + username, password := "foo", "bar" + + cases := []struct { + name string + createSecret bool + useMountSecrets bool + expected authn.Authenticator + }{ + { + name: "resolved successfully", + createSecret: true, + useMountSecrets: true, + expected: &authn.Basic{Username: username, Password: password}, + }, + { + name: "missing secret skipped", + createSecret: false, + useMountSecrets: true, + expected: &authn.Basic{}, + }, + { + name: "skip option", + createSecret: true, + useMountSecrets: false, + expected: &authn.Basic{}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + + objs := []runtime.Object{ + &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svcacct", + Namespace: "ns", + }, + Secrets: []corev1.ObjectReference{{ + Name: "secret", + }}, + }, + } + if c.createSecret { + objs = append(objs, dockerCfgSecretType.Create( + t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: username, + Password: password, + })) + } + client := fakeclient.NewSimpleClientset(objs...) + + kc, err := New(context.Background(), client, Options{ + Namespace: "ns", + ServiceAccountName: "svcacct", + UseMountSecrets: c.useMountSecrets, + }) + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), c.expected) + }) + } + +} + +// Prioritze picking the first secret +func TestSecretPriority(t *testing.T) { + secrets := []corev1.Secret{ + *dockerCfgSecretType.Create(t, "ns", "secret", "fake.registry.io", authn.AuthConfig{ + Username: "user", Password: "pass", + }), + *dockerCfgSecretType.Create(t, "ns", "secret-2", "fake.registry.io", authn.AuthConfig{ + Username: "anotherUser", Password: "anotherPass", + }), + } + + kc, err := NewFromPullSecrets(context.Background(), secrets) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + + expectedAuth := &authn.Basic{Username: "user", Password: "pass"} + testResolve(t, kc, registry(t, "fake.registry.io"), expectedAuth) +} + +func TestResolveTargets(t *testing.T) { + // Iterate over target types + targetTypes := []authn.Resource{ + registry(t, "fake.registry.io"), + repo(t, "fake.registry.io/repo"), + } + + for _, secretType := range dockerSecretTypes { + for _, target := range targetTypes { + // Drop the . + testName := secretType.key[1:] + "_" + target.String() + + t.Run(testName, func(t *testing.T) { + auth := authn.AuthConfig{ + Password: fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))), + Username: "user" + fmt.Sprintf("%x", md5.Sum([]byte(t.Name()))), + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *secretType.Create(t, "ns", "secret", target.String(), auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, target, authenticator) + }) + } + } +} + +func TestAuthWithScheme(t *testing.T) { + auth := authn.AuthConfig{ + Password: "password", + Username: "username", + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "https://fake.registry.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, registry(t, "fake.registry.io"), authenticator) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authenticator) +} + +func TestAuthWithPorts(t *testing.T) { + auth := authn.AuthConfig{ + Password: "password", + Username: "username", + } + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "fake.registry.io:5000", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + authenticator := &authn.Basic{Username: auth.Username, Password: auth.Password} + testResolve(t, kc, registry(t, "fake.registry.io:5000"), authenticator) + testResolve(t, kc, repo(t, "fake.registry.io:5000/repo"), authenticator) + + // Non-matching ports should return Anonymous + testResolve(t, kc, registry(t, "fake.registry.io:1000"), authn.Anonymous) + testResolve(t, kc, repo(t, "fake.registry.io:1000/repo"), authn.Anonymous) +} + +func TestAuthPathMatching(t *testing.T) { + rootAuth := authn.AuthConfig{Username: "root", Password: "root"} + nestedAuth := authn.AuthConfig{Username: "nested", Password: "nested"} + leafAuth := authn.AuthConfig{Username: "leaf", Password: "leaf"} + partialAuth := authn.AuthConfig{Username: "partial", Password: "partial"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "fake.registry.io/nested", nestedAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-3", "fake.registry.io/nested/repo", leafAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-4", "fake.registry.io/par", partialAuth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested"), authn.FromConfig(nestedAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested/repo"), authn.FromConfig(leafAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/nested/repo/dirt"), authn.FromConfig(leafAuth)) + testResolve(t, kc, repo(t, "fake.registry.io/partial"), authn.FromConfig(partialAuth)) +} + +func TestAuthHostNameVariations(t *testing.T) { + rootAuth := authn.AuthConfig{Username: "root", Password: "root"} + subdomainAuth := authn.AuthConfig{Username: "sub", Password: "sub"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "fake.registry.io", rootAuth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "1.fake.registry.io", subdomainAuth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(rootAuth)) + testResolve(t, kc, registry(t, "1.fake.registry.io"), authn.FromConfig(subdomainAuth)) + + // Unrecognized subdomain uses Anonymous + testResolve(t, kc, registry(t, "2.fake.registry.io"), authn.Anonymous) +} + +func TestAuthSpecialPathsIgnored(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + auth2 := authn.AuthConfig{Username: "root2", Password: "root2"} + + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + // Note the paths need a trailing '/' + *dockerConfigJSONSecretType.Create(t, "ns", "secret-1", "https://fake.registry.io/v1/", auth), + *dockerConfigJSONSecretType.Create(t, "ns", "secret-2", "https://fake2.registry.io/v2/", auth2), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth)) + testResolve(t, kc, registry(t, "fake2.registry.io"), authn.FromConfig(auth2)) + testResolve(t, kc, repo(t, "fake2.registry.io/repo"), authn.FromConfig(auth2)) +} + +func TestAuthDockerRegistry(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "index.docker.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, repo(t, "ubuntu"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "knative/serving"), authn.FromConfig(auth)) +} + +func TestAuthWithGlobs(t *testing.T) { + auth := authn.AuthConfig{Username: "root", Password: "root"} + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{ + *dockerConfigJSONSecretType.Create(t, "ns", "secret", "*.registry.io", auth), + }) + + if err != nil { + t.Fatalf("New() = %v", err) + } + + testResolve(t, kc, registry(t, "fake.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "fake.registry.io/repo"), authn.FromConfig(auth)) + testResolve(t, kc, registry(t, "blah.registry.io"), authn.FromConfig(auth)) + testResolve(t, kc, repo(t, "blah.registry.io/repo"), authn.FromConfig(auth)) +} + +func testResolve(t *testing.T, kc authn.Keychain, target authn.Resource, expectedAuth authn.Authenticator) { + t.Helper() + + auth, err := kc.Resolve(target) + if err != nil { + t.Errorf("Resolve(%v) = %v", target, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := expectedAuth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if diff := cmp.Diff(want, got); diff != "" { + t.Error("Resolve() diff (-want, +got)\n", diff) + } +} + +func toJSON(t *testing.T, obj any) []byte { + t.Helper() + + bites, err := json.Marshal(obj) + + if err != nil { + t.Fatal("unable to json marshal", err) + } + return bites +} + +func registry(t *testing.T, registry string) authn.Resource { + t.Helper() + + reg, err := name.NewRegistry(registry, name.WeakValidation) + if err != nil { + t.Fatal("failed to create registry", err) + } + return reg +} + +func repo(t *testing.T, repository string) authn.Resource { + t.Helper() + + repo, err := name.NewRepository(repository, name.WeakValidation) + if err != nil { + t.Fatal("failed to create repo", err) + } + return repo +} + +// TestDockerConfigJSON tests using secrets using the .dockerconfigjson form, +// like you might get from running: +// kubectl create secret docker-registry secret -n ns --docker-server="fake.registry.io" --docker-username="foo" --docker-password="bar" +func TestDockerConfigJSON(t *testing.T) { + username, password := "foo", "bar" + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte( + fmt.Sprintf(`{"auths":{"fake.registry.io":{"username":%q,"password":%q,"auth":%q}}}`, + username, password, + base64.StdEncoding.EncodeToString([]byte(username+":"+password))), + ), + }, + }}) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + + reg, err := name.NewRegistry("fake.registry.io", name.WeakValidation) + if err != nil { + t.Errorf("NewRegistry() = %v", err) + } + + auth, err := kc.Resolve(reg) + if err != nil { + t.Errorf("Resolve(%v) = %v", reg, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := (&authn.Basic{Username: username, Password: password}).Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Resolve() = %v, want %v", got, want) + } +} + +func TestKubernetesAuth(t *testing.T) { + // From https://github.com/knative/serving/issues/12761#issuecomment-1097441770 + // All of these should work with K8s' docker auth parsing. + for k, ss := range map[string][]string{ + "registry.gitlab.com/dprotaso/test/nginx": { + "registry.gitlab.com", + "http://registry.gitlab.com", + "https://registry.gitlab.com", + "registry.gitlab.com/dprotaso", + "http://registry.gitlab.com/dprotaso", + "https://registry.gitlab.com/dprotaso", + "registry.gitlab.com/dprotaso/test", + "http://registry.gitlab.com/dprotaso/test", + "https://registry.gitlab.com/dprotaso/test", + "registry.gitlab.com/dprotaso/test/nginx", + "http://registry.gitlab.com/dprotaso/test/nginx", + "https://registry.gitlab.com/dprotaso/test/nginx", + }, + "dtestcontainer.azurecr.io/dave/nginx": { + "dtestcontainer.azurecr.io", + "http://dtestcontainer.azurecr.io", + "https://dtestcontainer.azurecr.io", + "dtestcontainer.azurecr.io/dave", + "http://dtestcontainer.azurecr.io/dave", + "https://dtestcontainer.azurecr.io/dave", + "dtestcontainer.azurecr.io/dave/nginx", + "http://dtestcontainer.azurecr.io/dave/nginx", + "https://dtestcontainer.azurecr.io/dave/nginx", + }} { + repo, err := name.NewRepository(k) + if err != nil { + t.Errorf("parsing %q: %v", k, err) + continue + } + + for _, s := range ss { + t.Run(fmt.Sprintf("%s - %s", k, s), func(t *testing.T) { + username, password := "foo", "bar" + kc, err := NewFromPullSecrets(context.Background(), []corev1.Secret{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "secret", + Namespace: "ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: []byte( + fmt.Sprintf(`{"auths":{%q:{"username":%q,"password":%q,"auth":%q}}}`, + s, + username, password, + base64.StdEncoding.EncodeToString([]byte(username+":"+password))), + ), + }, + }}) + if err != nil { + t.Fatalf("NewFromPullSecrets() = %v", err) + } + auth, err := kc.Resolve(repo) + if err != nil { + t.Errorf("Resolve(%v) = %v", repo, err) + } + got, err := auth.Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + want, err := (&authn.Basic{Username: username, Password: password}).Authorization() + if err != nil { + t.Errorf("Authorization() = %v", err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("Resolve() = %v, want %v", got, want) + } + }) + } + } +} diff --git a/pkg/authn/multikeychain.go b/pkg/authn/multikeychain.go new file mode 100644 index 0000000..3b1804f --- /dev/null +++ b/pkg/authn/multikeychain.go @@ -0,0 +1,41 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +type multiKeychain struct { + keychains []Keychain +} + +// Assert that our multi-keychain implements Keychain. +var _ (Keychain) = (*multiKeychain)(nil) + +// NewMultiKeychain composes a list of keychains into one new keychain. +func NewMultiKeychain(kcs ...Keychain) Keychain { + return &multiKeychain{keychains: kcs} +} + +// Resolve implements Keychain. +func (mk *multiKeychain) Resolve(target Resource) (Authenticator, error) { + for _, kc := range mk.keychains { + auth, err := kc.Resolve(target) + if err != nil { + return nil, err + } + if auth != Anonymous { + return auth, nil + } + } + return Anonymous, nil +} diff --git a/pkg/authn/multikeychain_test.go b/pkg/authn/multikeychain_test.go new file mode 100644 index 0000000..3dff0c9 --- /dev/null +++ b/pkg/authn/multikeychain_test.go @@ -0,0 +1,98 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authn + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" +) + +func TestMultiKeychain(t *testing.T) { + one := &Basic{Username: "one", Password: "secret"} + two := &Basic{Username: "two", Password: "secret"} + three := &Basic{Username: "three", Password: "secret"} + + regOne, _ := name.NewRegistry("one.gcr.io", name.StrictValidation) + regTwo, _ := name.NewRegistry("two.gcr.io", name.StrictValidation) + regThree, _ := name.NewRegistry("three.gcr.io", name.StrictValidation) + + tests := []struct { + name string + reg name.Registry + kc Keychain + want Authenticator + }{{ + // Make sure our test keychain WAI + name: "simple fixed test (match)", + reg: regOne, + kc: fixedKeychain{regOne: one}, + want: one, + }, { + // Make sure our test keychain WAI + name: "simple fixed test (no match)", + reg: regTwo, + kc: fixedKeychain{regOne: one}, + want: Anonymous, + }, { + name: "match first keychain", + reg: regOne, + kc: NewMultiKeychain( + fixedKeychain{regOne: one}, + fixedKeychain{regOne: three, regTwo: two}, + ), + want: one, + }, { + name: "match second keychain", + reg: regTwo, + kc: NewMultiKeychain( + fixedKeychain{regOne: one}, + fixedKeychain{regOne: three, regTwo: two}, + ), + want: two, + }, { + name: "match no keychain", + reg: regThree, + kc: NewMultiKeychain( + fixedKeychain{regOne: one}, + fixedKeychain{regOne: three, regTwo: two}, + ), + want: Anonymous, + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.kc.Resolve(test.reg) + if err != nil { + t.Errorf("Resolve() = %v", err) + } + if got != test.want { + t.Errorf("Resolve() = %v, wanted %v", got, test.want) + } + }) + } +} + +type fixedKeychain map[Resource]Authenticator + +var _ Keychain = (fixedKeychain)(nil) + +// Resolve implements Keychain. +func (fk fixedKeychain) Resolve(target Resource) (Authenticator, error) { + if auth, ok := fk[target]; ok { + return auth, nil + } + return Anonymous, nil +} diff --git a/pkg/compression/compression.go b/pkg/compression/compression.go new file mode 100644 index 0000000..6686c2d --- /dev/null +++ b/pkg/compression/compression.go @@ -0,0 +1,26 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package compression abstracts over gzip and zstd. +package compression + +// Compression is an enumeration of the supported compression algorithms +type Compression string + +// The collection of known MediaType values. +const ( + None Compression = "none" + GZip Compression = "gzip" + ZStd Compression = "zstd" +) diff --git a/pkg/crane/append.go b/pkg/crane/append.go new file mode 100644 index 0000000..f1c2ef6 --- /dev/null +++ b/pkg/crane/append.go @@ -0,0 +1,114 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + "os" + + "github.com/google/go-containerregistry/internal/windows" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func isWindows(img v1.Image) (bool, error) { + cfg, err := img.ConfigFile() + if err != nil { + return false, err + } + return cfg != nil && cfg.OS == "windows", nil +} + +// Append reads a layer from path and appends it the the v1.Image base. +// +// If the base image is a Windows base image (i.e., its config.OS is +// "windows"), the contents of the tarballs will be modified to be suitable for +// a Windows container image.`, +func Append(base v1.Image, paths ...string) (v1.Image, error) { + if base == nil { + return nil, fmt.Errorf("invalid argument: base") + } + + win, err := isWindows(base) + if err != nil { + return nil, fmt.Errorf("getting base image: %w", err) + } + + baseMediaType, err := base.MediaType() + + if err != nil { + return nil, fmt.Errorf("getting base image media type: %w", err) + } + + layerType := types.DockerLayer + + if baseMediaType == types.OCIManifestSchema1 { + layerType = types.OCILayer + } + + layers := make([]v1.Layer, 0, len(paths)) + for _, path := range paths { + layer, err := getLayer(path, layerType) + if err != nil { + return nil, fmt.Errorf("reading layer %q: %w", path, err) + } + + if win { + layer, err = windows.Windows(layer) + if err != nil { + return nil, fmt.Errorf("converting %q for Windows: %w", path, err) + } + } + + layers = append(layers, layer) + } + + return mutate.AppendLayers(base, layers...) +} + +func getLayer(path string, layerType types.MediaType) (v1.Layer, error) { + f, err := streamFile(path) + if err != nil { + return nil, err + } + if f != nil { + return stream.NewLayer(f, stream.WithMediaType(layerType)), nil + } + + return tarball.LayerFromFile(path, tarball.WithMediaType(layerType)) +} + +// If we're dealing with a named pipe, trying to open it multiple times will +// fail, so we need to do a streaming upload. +// +// returns nil, nil for non-streaming files +func streamFile(path string) (*os.File, error) { + if path == "-" { + return os.Stdin, nil + } + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if !fi.Mode().IsRegular() { + return os.Open(path) + } + + return nil, nil +} diff --git a/pkg/crane/append_test.go b/pkg/crane/append_test.go new file mode 100644 index 0000000..d894a9d --- /dev/null +++ b/pkg/crane/append_test.go @@ -0,0 +1,73 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestAppendWithOCIBaseImage(t *testing.T) { + base := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + img, err := crane.Append(base, "testdata/content.tar") + + if err != nil { + t.Fatalf("crane.Append(): %v", err) + } + + layers, err := img.Layers() + + if err != nil { + t.Fatalf("img.Layers(): %v", err) + } + + mediaType, err := layers[0].MediaType() + + if err != nil { + t.Fatalf("layers[0].MediaType(): %v", err) + } + + if got, want := mediaType, types.OCILayer; got != want { + t.Errorf("MediaType(): want %q, got %q", want, got) + } +} + +func TestAppendWithDockerBaseImage(t *testing.T) { + img, err := crane.Append(empty.Image, "testdata/content.tar") + + if err != nil { + t.Fatalf("crane.Append(): %v", err) + } + + layers, err := img.Layers() + + if err != nil { + t.Fatalf("img.Layers(): %v", err) + } + + mediaType, err := layers[0].MediaType() + + if err != nil { + t.Fatalf("layers[0].MediaType(): %v", err) + } + + if got, want := mediaType, types.DockerLayer; got != want { + t.Errorf("MediaType(): want %q, got %q", want, got) + } +} diff --git a/pkg/crane/catalog.go b/pkg/crane/catalog.go new file mode 100644 index 0000000..f30800c --- /dev/null +++ b/pkg/crane/catalog.go @@ -0,0 +1,35 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Catalog returns the repositories in a registry's catalog. +func Catalog(src string, opt ...Option) (res []string, err error) { + o := makeOptions(opt...) + reg, err := name.NewRegistry(src, o.Name...) + if err != nil { + return nil, err + } + + // This context gets overridden by remote.WithContext, which is set by + // crane.WithContext. + return remote.Catalog(context.Background(), reg, o.Remote...) +} diff --git a/pkg/crane/config.go b/pkg/crane/config.go new file mode 100644 index 0000000..3e55cc9 --- /dev/null +++ b/pkg/crane/config.go @@ -0,0 +1,24 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +// Config returns the config file for the remote image ref. +func Config(ref string, opt ...Option) ([]byte, error) { + i, _, err := getImage(ref, opt...) + if err != nil { + return nil, err + } + return i.RawConfigFile() +} diff --git a/pkg/crane/copy.go b/pkg/crane/copy.go new file mode 100644 index 0000000..a606f96 --- /dev/null +++ b/pkg/crane/copy.go @@ -0,0 +1,88 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/internal/legacy" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Copy copies a remote image or index from src to dst. +func Copy(src, dst string, opt ...Option) error { + o := makeOptions(opt...) + srcRef, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference for %q: %w", dst, err) + } + + logs.Progress.Printf("Copying from %v to %v", srcRef, dstRef) + desc, err := remote.Get(srcRef, o.Remote...) + if err != nil { + return fmt.Errorf("fetching %q: %w", src, err) + } + + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + // Handle indexes separately. + if o.Platform != nil { + // If platform is explicitly set, don't copy the whole index, just the appropriate image. + if err := copyImage(desc, dstRef, o); err != nil { + return fmt.Errorf("failed to copy image: %w", err) + } + } else { + if err := copyIndex(desc, dstRef, o); err != nil { + return fmt.Errorf("failed to copy index: %w", err) + } + } + case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: + // Handle schema 1 images separately. + if err := legacy.CopySchema1(desc, srcRef, dstRef, o.Remote...); err != nil { + return fmt.Errorf("failed to copy schema 1 image: %w", err) + } + default: + // Assume anything else is an image, since some registries don't set mediaTypes properly. + if err := copyImage(desc, dstRef, o); err != nil { + return fmt.Errorf("failed to copy image: %w", err) + } + } + + return nil +} + +func copyImage(desc *remote.Descriptor, dstRef name.Reference, o Options) error { + img, err := desc.Image() + if err != nil { + return err + } + return remote.Write(dstRef, img, o.Remote...) +} + +func copyIndex(desc *remote.Descriptor, dstRef name.Reference, o Options) error { + idx, err := desc.ImageIndex() + if err != nil { + return err + } + return remote.WriteIndex(dstRef, idx, o.Remote...) +} diff --git a/pkg/crane/crane_test.go b/pkg/crane/crane_test.go new file mode 100644 index 0000000..9f4d124 --- /dev/null +++ b/pkg/crane/crane_test.go @@ -0,0 +1,574 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane_test + +import ( + "archive/tar" + "bytes" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// TODO(jonjohnsonjr): Test crane.Copy failures. +func TestCraneRegistry(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + src := fmt.Sprintf("%s/test/crane", u.Host) + dst := fmt.Sprintf("%s/test/crane/copy", u.Host) + + // Expected values. + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + digest, err := img.Digest() + if err != nil { + t.Fatal(err) + } + rawManifest, err := img.RawManifest() + if err != nil { + t.Fatal(err) + } + manifest, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + config, err := img.RawConfigFile() + if err != nil { + t.Fatal(err) + } + layer, err := img.LayerByDigest(manifest.Layers[0].Digest) + if err != nil { + t.Fatal(err) + } + + // Load up the registry. + if err := crane.Push(img, src); err != nil { + t.Fatal(err) + } + + // Test that `crane.Foo` returns expected values. + d, err := crane.Digest(src) + if err != nil { + t.Error(err) + } else if d != digest.String() { + t.Errorf("Digest(): %v != %v", d, digest) + } + + m, err := crane.Manifest(src) + if err != nil { + t.Error(err) + } else if string(m) != string(rawManifest) { + t.Errorf("Manifest(): %v != %v", m, rawManifest) + } + + c, err := crane.Config(src) + if err != nil { + t.Error(err) + } else if string(c) != string(config) { + t.Errorf("Config(): %v != %v", c, config) + } + + // Make sure we pull what we pushed. + pulled, err := crane.Pull(src) + if err != nil { + t.Error(err) + } + if err := compare.Images(img, pulled); err != nil { + t.Fatal(err) + } + + // Test that the copied image is the same as the source. + if err := crane.Copy(src, dst); err != nil { + t.Fatal(err) + } + + // Make sure what we copied is equivalent. + // Also, get options coverage in a dumb way. + copied, err := crane.Pull(dst, crane.Insecure, crane.WithTransport(http.DefaultTransport), crane.WithAuth(authn.Anonymous), crane.WithAuthFromKeychain(authn.DefaultKeychain), crane.WithUserAgent("crane/tests")) + if err != nil { + t.Fatal(err) + } + if err := compare.Images(pulled, copied); err != nil { + t.Fatal(err) + } + + if err := crane.Tag(dst, "crane-tag"); err != nil { + t.Fatal(err) + } + + // Make sure what we tagged is equivalent. + tagged, err := crane.Pull(fmt.Sprintf("%s:%s", dst, "crane-tag")) + if err != nil { + t.Fatal(err) + } + if err := compare.Images(pulled, tagged); err != nil { + t.Fatal(err) + } + + layerRef := fmt.Sprintf("%s/test/crane@%s", u.Host, manifest.Layers[0].Digest) + pulledLayer, err := crane.PullLayer(layerRef) + if err != nil { + t.Fatal(err) + } + + if err := compare.Layers(pulledLayer, layer); err != nil { + t.Fatal(err) + } + + // List Tags + // dst variable have: latest and crane-tag + tags, err := crane.ListTags(dst) + if err != nil { + t.Fatal(err) + } + if len(tags) != 2 { + t.Fatalf("wanted 2 tags, got %d", len(tags)) + } + + // create 4 tags for dst + for i := 1; i < 5; i++ { + if err := crane.Tag(dst, fmt.Sprintf("honk-tag-%d", i)); err != nil { + t.Fatal(err) + } + } + + tags, err = crane.ListTags(dst) + if err != nil { + t.Fatal(err) + } + if len(tags) != 6 { + t.Fatalf("wanted 6 tags, got %d", len(tags)) + } + + // Delete the non existing image + if err := crane.Delete(dst + ":honk-image"); err == nil { + t.Fatal("wanted err, got nil") + } + + // Delete the image + if err := crane.Delete(src); err != nil { + t.Fatal(err) + } + + // check if the image was really deleted + if _, err := crane.Pull(src); err == nil { + t.Fatal("wanted err, got nil") + } + + // check if the copied image still exist + dstPulled, err := crane.Pull(dst) + if err != nil { + t.Fatal(err) + } + if err := compare.Images(dstPulled, copied); err != nil { + t.Fatal(err) + } + + // List Catalog + repos, err := crane.Catalog(u.Host) + if err != nil { + t.Fatal(err) + } + if len(repos) != 2 { + t.Fatalf("wanted 2 repos, got %d", len(repos)) + } + + // Test pushing layer + layer, err = img.LayerByDigest(manifest.Layers[1].Digest) + if err != nil { + t.Fatal(err) + } + if err := crane.Upload(layer, dst); err != nil { + t.Fatal(err) + } +} + +func TestCraneCopyIndex(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + src := fmt.Sprintf("%s/test/crane", u.Host) + dst := fmt.Sprintf("%s/test/crane/copy", u.Host) + + // Load up the registry. + idx, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + ref, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + if err := remote.WriteIndex(ref, idx); err != nil { + t.Fatal(err) + } + + // Test that the copied index is the same as the source. + if err := crane.Copy(src, dst); err != nil { + t.Fatal(err) + } + + d, err := crane.Digest(src) + if err != nil { + t.Fatal(err) + } + cp, err := crane.Digest(dst) + if err != nil { + t.Fatal(err) + } + if d != cp { + t.Errorf("Copied Digest(): %v != %v", d, cp) + } +} + +func TestWithPlatform(t *testing.T) { + // Set up a fake registry with a platform-specific image. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + imgs := []mutate.IndexAddendum{} + for _, plat := range []string{ + "linux/amd64", + "linux/arm", + } { + img, err := crane.Image(map[string][]byte{ + "platform.txt": []byte(plat), + }) + if err != nil { + t.Fatal(err) + } + parts := strings.Split(plat, "/") + imgs = append(imgs, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + Platform: &v1.Platform{ + OS: parts[0], + Architecture: parts[1], + }, + }, + }) + } + + idx := mutate.AppendManifests(empty.Index, imgs...) + + src := path.Join(u.Host, "src") + dst := path.Join(u.Host, "dst") + + ref, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + + // Populate registry so we can copy from it. + if err := remote.WriteIndex(ref, idx); err != nil { + t.Fatal(err) + } + + if err := crane.Copy(src, dst, crane.WithPlatform(imgs[1].Platform)); err != nil { + t.Fatal(err) + } + + want, err := crane.Manifest(src, crane.WithPlatform(imgs[1].Platform)) + if err != nil { + t.Fatal(err) + } + got, err := crane.Manifest(dst) + if err != nil { + t.Fatal(err) + } + + if string(got) != string(want) { + t.Errorf("Manifest(%q) != Manifest(%q): (\n\n%s\n\n!=\n\n%s\n\n)", dst, src, string(got), string(want)) + } + + arch := "real fake doors" + + // Now do a fake platform, should fail + if _, err := crane.Manifest(src, crane.WithPlatform(&v1.Platform{ + OS: "does-not-exist", + Architecture: arch, + })); err == nil { + t.Error("crane.Manifest(fake platform): got nil want err") + } else if !strings.Contains(err.Error(), arch) { + t.Errorf("crane.Manifest(fake platform): expected %q in error, got: %v", arch, err) + } +} + +func TestCraneTarball(t *testing.T) { + t.Parallel() + // Write an image as a tarball. + tmp, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmp.Name()) + + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + digest, err := img.Digest() + if err != nil { + t.Fatal(err) + } + src := fmt.Sprintf("test/crane@%s", digest) + + if err := crane.Save(img, src, tmp.Name()); err != nil { + t.Errorf("Save: %v", err) + } + + // Make sure the image we load has a matching digest. + img, err = crane.Load(tmp.Name()) + if err != nil { + t.Fatal(err) + } + + d, err := img.Digest() + if err != nil { + t.Fatal(err) + } + if d != digest { + t.Errorf("digest mismatch: %v != %v", d, digest) + } +} + +func TestCraneSaveLegacy(t *testing.T) { + t.Parallel() + // Write an image as a legacy tarball. + tmp, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmp.Name()) + + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + + if err := crane.SaveLegacy(img, "test/crane", tmp.Name()); err != nil { + t.Errorf("SaveOCI: %v", err) + } +} + +func TestCraneSaveOCI(t *testing.T) { + t.Parallel() + // Write an image as an OCI image layout. + tmp := t.TempDir() + + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + if err := crane.SaveOCI(img, tmp); err != nil { + t.Errorf("SaveLegacy: %v", err) + } +} + +func TestCraneFilesystem(t *testing.T) { + t.Parallel() + tmp, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + + name := "/some/file" + content := []byte("sentinel") + + tw := tar.NewWriter(tmp) + if err := tw.WriteHeader(&tar.Header{ + Size: int64(len(content)), + Name: name, + }); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(content); err != nil { + t.Fatal(err) + } + tw.Flush() + tw.Close() + + img, err = crane.Append(img, tmp.Name()) + if err != nil { + t.Fatal(err) + } + + var buf bytes.Buffer + if err := crane.Export(img, &buf); err != nil { + t.Fatal(err) + } + + tr := tar.NewReader(&buf) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + t.Fatalf("didn't find find") + } else if err != nil { + t.Fatal(err) + } + if header.Name == name { + b, err := io.ReadAll(tr) + if err != nil { + t.Fatal(err) + } + if string(b) != string(content) { + t.Fatalf("got back wrong content: %v != %v", string(b), string(content)) + } + break + } + } +} + +func TestStreamingAppend(t *testing.T) { + // Stdin will be an uncompressed layer. + layer, err := crane.Layer(map[string][]byte{ + "hello": []byte(`world`), + }) + if err != nil { + t.Fatal(err) + } + rc, err := layer.Uncompressed() + if err != nil { + t.Fatal(err) + } + + tmp, err := os.CreateTemp("", "crane-append") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmp.Name()) + + if _, err := io.Copy(tmp, rc); err != nil { + t.Fatal(err) + } + + stdin := os.Stdin + defer func() { + os.Stdin = stdin + }() + + os.Stdin = tmp + + img, err := crane.Append(empty.Image, "-") + if err != nil { + t.Fatal(err) + } + ll, err := img.Layers() + if err != nil { + t.Fatal(err) + } + if want, got := 1, len(ll); want != got { + t.Errorf("crane.Append(stdin) - len(layers): want %d != got %d", want, got) + } +} + +func TestBadInputs(t *testing.T) { + t.Parallel() + invalid := "/dev/null/@@@@@@" + + // Create a valid image reference that will fail with not found. + s := httptest.NewServer(http.NotFoundHandler()) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + valid404 := fmt.Sprintf("%s/some/image", u.Host) + + // e drops the first parameter so we can use the result of a function + // that returns two values as an expression above. This is a bit of a go quirk. + e := func(_ any, err error) error { + return err + } + + for _, tc := range []struct { + desc string + err error + }{ + {"Push(_, invalid)", crane.Push(nil, invalid)}, + {"Upload(_, invalid)", crane.Upload(nil, invalid)}, + {"Delete(invalid)", crane.Delete(invalid)}, + {"Delete: 404", crane.Delete(valid404)}, + {"Save(_, invalid)", crane.Save(nil, invalid, "")}, + {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, invalid, "")}, + {"SaveLegacy(_, invalid)", crane.SaveLegacy(nil, valid404, invalid)}, + {"SaveOCI(_, invalid)", crane.SaveOCI(nil, "")}, + {"Copy(invalid, invalid)", crane.Copy(invalid, invalid)}, + {"Copy(404, invalid)", crane.Copy(valid404, invalid)}, + {"Copy(404, 404)", crane.Copy(valid404, valid404)}, + {"Tag(invalid, invalid)", crane.Tag(invalid, invalid)}, + {"Tag(404, invalid)", crane.Tag(valid404, invalid)}, + {"Tag(404, 404)", crane.Tag(valid404, valid404)}, + {"Optimize(invalid, invalid)", crane.Optimize(invalid, invalid, []string{})}, + {"Optimize(404, invalid)", crane.Optimize(valid404, invalid, []string{})}, + {"Optimize(404, 404)", crane.Optimize(valid404, valid404, []string{})}, + // These return multiple values, which are hard to use as expressions. + {"Pull(invalid)", e(crane.Pull(invalid))}, + {"Digest(invalid)", e(crane.Digest(invalid))}, + {"Manifest(invalid)", e(crane.Manifest(invalid))}, + {"Config(invalid)", e(crane.Config(invalid))}, + {"Config(404)", e(crane.Config(valid404))}, + {"ListTags(invalid)", e(crane.ListTags(invalid))}, + {"ListTags(404)", e(crane.ListTags(valid404))}, + {"Append(_, invalid)", e(crane.Append(nil, invalid))}, + {"Catalog(invalid)", e(crane.Catalog(invalid))}, + {"Catalog(404)", e(crane.Catalog(u.Host))}, + {"PullLayer(invalid)", e(crane.PullLayer(invalid))}, + {"LoadTag(_, invalid)", e(crane.LoadTag("", invalid))}, + {"LoadTag(invalid, 404)", e(crane.LoadTag(invalid, valid404))}, + } { + if tc.err == nil { + t.Errorf("%s: expected err, got nil", tc.desc) + } + } +} diff --git a/pkg/crane/delete.go b/pkg/crane/delete.go new file mode 100644 index 0000000..58a8be1 --- /dev/null +++ b/pkg/crane/delete.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Delete deletes the remote reference at src. +func Delete(src string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + return remote.Delete(ref, o.Remote...) +} diff --git a/pkg/crane/digest.go b/pkg/crane/digest.go new file mode 100644 index 0000000..868a570 --- /dev/null +++ b/pkg/crane/digest.go @@ -0,0 +1,52 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import "github.com/google/go-containerregistry/pkg/logs" + +// Digest returns the sha256 hash of the remote image at ref. +func Digest(ref string, opt ...Option) (string, error) { + o := makeOptions(opt...) + if o.Platform != nil { + desc, err := getManifest(ref, opt...) + if err != nil { + return "", err + } + if !desc.MediaType.IsIndex() { + return desc.Digest.String(), nil + } + + // TODO: does not work for indexes which contain schema v1 manifests + img, err := desc.Image() + if err != nil { + return "", err + } + digest, err := img.Digest() + if err != nil { + return "", err + } + return digest.String(), nil + } + desc, err := Head(ref, opt...) + if err != nil { + logs.Warn.Printf("HEAD request failed, falling back on GET: %v", err) + rdesc, err := getManifest(ref, opt...) + if err != nil { + return "", err + } + return rdesc.Digest.String(), nil + } + return desc.Digest.String(), nil +} diff --git a/pkg/crane/digest_test.go b/pkg/crane/digest_test.go new file mode 100644 index 0000000..ac215d3 --- /dev/null +++ b/pkg/crane/digest_test.go @@ -0,0 +1,61 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestDigest_MissingDigest(t *testing.T) { + response := []byte("doesn't matter") + digest := "sha256:477c34d98f9e090a4441cf82d2f1f03e64c8eb730e8c1ef39a8595e685d4df65" // Digest of "doesn't matter" + getCalled := false + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v2/" { + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Content-Type", string(types.DockerManifestSchema2)) + if r.Method == http.MethodGet { + getCalled = true + w.Header().Set("Docker-Content-Digest", digest) + } + // This will automatically set the Content-Length header. + w.Write(response) + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + got, err := Digest(fmt.Sprintf("%s/repo:latest", u.Host)) + if err != nil { + t.Fatalf("Digest: %v", err) + } + if got != digest { + t.Errorf("Digest: got %q, want %q", got, digest) + } + if !getCalled { + t.Errorf("Digest: expected GET to be called") + } +} diff --git a/pkg/crane/doc.go b/pkg/crane/doc.go new file mode 100644 index 0000000..7602d79 --- /dev/null +++ b/pkg/crane/doc.go @@ -0,0 +1,16 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package crane holds libraries used to implement the crane CLI. +package crane diff --git a/pkg/crane/example_test.go b/pkg/crane/example_test.go new file mode 100644 index 0000000..3a6c182 --- /dev/null +++ b/pkg/crane/example_test.go @@ -0,0 +1,31 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane_test + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/crane" +) + +func Example() { + c := map[string][]byte{ + "/binary": []byte("binary contents"), + } + i, _ := crane.Image(c) + d, _ := i.Digest() + fmt.Println(d) + // Output: sha256:09fb0c6289cefaad8c74c7e5fd6758ad6906ab8f57f1350d9f4eb5a7df45ff8b +} diff --git a/pkg/crane/export.go b/pkg/crane/export.go new file mode 100644 index 0000000..5d6da1d --- /dev/null +++ b/pkg/crane/export.go @@ -0,0 +1,47 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "io" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" +) + +// Export writes the filesystem contents (as a tarball) of img to w. +// If img has a single layer, just write the (uncompressed) contents to w so +// that this "just works" for images that just wrap a single blob. +func Export(img v1.Image, w io.Writer) error { + layers, err := img.Layers() + if err != nil { + return err + } + if len(layers) == 1 { + // If it's a single layer, we don't have to flatten the filesystem. + // An added perk of skipping mutate.Extract here is that this works + // for non-tarball layers. + l := layers[0] + rc, err := l.Uncompressed() + if err != nil { + return err + } + _, err = io.Copy(w, rc) + return err + } + fs := mutate.Extract(img) + _, err = io.Copy(w, fs) + return err +} diff --git a/pkg/crane/export_test.go b/pkg/crane/export_test.go new file mode 100644 index 0000000..e60e941 --- /dev/null +++ b/pkg/crane/export_test.go @@ -0,0 +1,41 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "bytes" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestExport(t *testing.T) { + want := []byte(`{"foo":"bar"}`) + layer := static.NewLayer(want, types.MediaType("application/json")) + img, err := mutate.AppendLayers(empty.Image, layer) + if err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + if err := Export(img, &buf); err != nil { + t.Fatal(err) + } + if got := buf.Bytes(); !bytes.Equal(got, want) { + t.Errorf("got: %s\nwant: %s", got, want) + } +} diff --git a/pkg/crane/filemap.go b/pkg/crane/filemap.go new file mode 100644 index 0000000..36dfc2a --- /dev/null +++ b/pkg/crane/filemap.go @@ -0,0 +1,72 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "archive/tar" + "bytes" + "io" + "sort" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Layer creates a layer from a single file map. These layers are reproducible and consistent. +// A filemap is a path -> file content map representing a file system. +func Layer(filemap map[string][]byte) (v1.Layer, error) { + b := &bytes.Buffer{} + w := tar.NewWriter(b) + + fn := []string{} + for f := range filemap { + fn = append(fn, f) + } + sort.Strings(fn) + + for _, f := range fn { + c := filemap[f] + if err := w.WriteHeader(&tar.Header{ + Name: f, + Size: int64(len(c)), + }); err != nil { + return nil, err + } + if _, err := w.Write(c); err != nil { + return nil, err + } + } + if err := w.Close(); err != nil { + return nil, err + } + + // Return a new copy of the buffer each time it's opened. + return tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(b.Bytes())), nil + }) +} + +// Image creates a image with the given filemaps as its contents. These images are reproducible and consistent. +// A filemap is a path -> file content map representing a file system. +func Image(filemap map[string][]byte) (v1.Image, error) { + y, err := Layer(filemap) + if err != nil { + return nil, err + } + + return mutate.AppendLayers(empty.Image, y) +} diff --git a/pkg/crane/filemap_test.go b/pkg/crane/filemap_test.go new file mode 100644 index 0000000..21d8d54 --- /dev/null +++ b/pkg/crane/filemap_test.go @@ -0,0 +1,187 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane_test + +import ( + "archive/tar" + "errors" + "io" + "testing" + + "github.com/google/go-containerregistry/pkg/crane" +) + +func TestLayer(t *testing.T) { + tcs := []struct { + Name string + FileMap map[string][]byte + Digest string + }{{ + Name: "Empty contents", + Digest: "sha256:89732bc7504122601f40269fc9ddfb70982e633ea9caf641ae45736f2846b004", + }, { + Name: "One file", + FileMap: map[string][]byte{ + "/test": []byte("testy"), + }, + Digest: "sha256:ec3ff19f471b99a76fb1c339c1dfdaa944a4fba25be6bcdc99fe7e772103079e", + }, { + Name: "Two files", + FileMap: map[string][]byte{ + "/test": []byte("testy"), + "/testalt": []byte("footesty"), + }, + Digest: "sha256:a48bcb7be3ab3ec608ee56eb80901224e19e31dc096cc06a8fd3a8dae1aa8947", + }, { + Name: "Many files", + FileMap: map[string][]byte{ + "/1": []byte("1"), + "/2": []byte("2"), + "/3": []byte("3"), + "/4": []byte("4"), + "/5": []byte("5"), + "/6": []byte("6"), + "/7": []byte("7"), + "/8": []byte("8"), + "/9": []byte("9"), + }, + Digest: "sha256:1e637602abbcab2dcedcc24e0b7c19763454a47261f1658b57569530b369ccb9", + }} + + for _, tc := range tcs { + t.Run(tc.Name, func(t *testing.T) { + l, err := crane.Layer(tc.FileMap) + if err != nil { + t.Fatalf("Error calling layer: %v", err) + } + + d, err := l.Digest() + if err != nil { + t.Fatalf("Error calling digest: %v", err) + } + if d.String() != tc.Digest { + t.Errorf("Incorrect digest, want %q, got %q", tc.Digest, d.String()) + } + + // Check contents match. + rc, err := l.Uncompressed() + if err != nil { + t.Fatalf("Uncompressed: %v", err) + } + defer rc.Close() + tr := tar.NewReader(rc) + saw := map[string]struct{}{} + for { + th, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("Next: %v", err) + } + saw[th.Name] = struct{}{} + want, found := tc.FileMap[th.Name] + if !found { + t.Errorf("found %q, not in original map", th.Name) + continue + } + got, err := io.ReadAll(tr) + if err != nil { + t.Fatalf("ReadAll(%q): %v", th.Name, err) + } + if string(want) != string(got) { + t.Errorf("File %q: got %v, want %v", th.Name, string(got), string(want)) + } + } + for k := range saw { + delete(tc.FileMap, k) + } + for k := range tc.FileMap { + t.Errorf("Layer did not contain %q", k) + } + }) + t.Run(tc.Name+" is reproducible", func(t *testing.T) { + l1, _ := crane.Layer(tc.FileMap) + l2, _ := crane.Layer(tc.FileMap) + d1, _ := l1.Digest() + d2, _ := l2.Digest() + if d1 != d2 { + t.Fatalf("Non matching digests, want %q, got %q", d1, d2) + } + }) + } +} + +func TestImage(t *testing.T) { + tcs := []struct { + Name string + FileMap map[string][]byte + Digest string + }{{ + Name: "Empty contents", + Digest: "sha256:98132f58b523c391a5788997327cac95e114e3a6609d01163189774510705399", + }, { + Name: "One file", + FileMap: map[string][]byte{ + "/test": []byte("testy"), + }, + Digest: "sha256:d905c03ac635172a96c12b8af6c90cfd028e3edaa3114b31a9e196ab38c16963", + }, { + Name: "Two files", + FileMap: map[string][]byte{ + "/test": []byte("testy"), + "/bar": []byte("not useful"), + }, + Digest: "sha256:20e7e4800e5eb167f170970936c08d9e1bcbe91372420eeb6ab8d1a07752c3a3", + }, { + Name: "Many files", + FileMap: map[string][]byte{ + "/1": []byte("1"), + "/2": []byte("2"), + "/3": []byte("3"), + "/4": []byte("4"), + "/5": []byte("5"), + "/6": []byte("6"), + "/7": []byte("7"), + "/8": []byte("8"), + "/9": []byte("9"), + }, + Digest: "sha256:dfca2803510c8e3b83a3151f7c035c60cfa2a8a52465b802e18b85014de361f1", + }} + for _, tc := range tcs { + t.Run(tc.Name, func(t *testing.T) { + i, err := crane.Image(tc.FileMap) + if err != nil { + t.Fatalf("Error calling image: %v", err) + } + d, err := i.Digest() + if err != nil { + t.Fatalf("Error calling digest: %v", err) + } + if d.String() != tc.Digest { + t.Fatalf("Incorrect digest, want %q, got %q", tc.Digest, d.String()) + } + }) + t.Run(tc.Name+" is reproducible", func(t *testing.T) { + i1, _ := crane.Image(tc.FileMap) + i2, _ := crane.Image(tc.FileMap) + d1, _ := i1.Digest() + d2, _ := i2.Digest() + if d1 != d2 { + t.Fatalf("Non matching digests, want %q, got %q", d1, d2) + } + }) + } +} diff --git a/pkg/crane/get.go b/pkg/crane/get.go new file mode 100644 index 0000000..1f12f01 --- /dev/null +++ b/pkg/crane/get.go @@ -0,0 +1,56 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func getImage(r string, opt ...Option) (v1.Image, name.Reference, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, nil, fmt.Errorf("parsing reference %q: %w", r, err) + } + img, err := remote.Image(ref, o.Remote...) + if err != nil { + return nil, nil, fmt.Errorf("reading image %q: %w", ref, err) + } + return img, ref, nil +} + +func getManifest(r string, opt ...Option) (*remote.Descriptor, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing reference %q: %w", r, err) + } + return remote.Get(ref, o.Remote...) +} + +// Head performs a HEAD request for a manifest and returns a content descriptor +// based on the registry's response. +func Head(r string, opt ...Option) (*v1.Descriptor, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(r, o.Name...) + if err != nil { + return nil, err + } + return remote.Head(ref, o.Remote...) +} diff --git a/pkg/crane/list.go b/pkg/crane/list.go new file mode 100644 index 0000000..3835215 --- /dev/null +++ b/pkg/crane/list.go @@ -0,0 +1,33 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// ListTags returns the tags in repository src. +func ListTags(src string, opt ...Option) ([]string, error) { + o := makeOptions(opt...) + repo, err := name.NewRepository(src, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing repo %q: %w", src, err) + } + + return remote.List(repo, o.Remote...) +} diff --git a/pkg/crane/manifest.go b/pkg/crane/manifest.go new file mode 100644 index 0000000..a54926a --- /dev/null +++ b/pkg/crane/manifest.go @@ -0,0 +1,32 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +// Manifest returns the manifest for the remote image or index ref. +func Manifest(ref string, opt ...Option) ([]byte, error) { + desc, err := getManifest(ref, opt...) + if err != nil { + return nil, err + } + o := makeOptions(opt...) + if o.Platform != nil { + img, err := desc.Image() + if err != nil { + return nil, err + } + return img.RawManifest() + } + return desc.Manifest, nil +} diff --git a/pkg/crane/optimize.go b/pkg/crane/optimize.go new file mode 100644 index 0000000..74c665d --- /dev/null +++ b/pkg/crane/optimize.go @@ -0,0 +1,237 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "errors" + "fmt" + + "github.com/containerd/stargz-snapshotter/estargz" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Optimize optimizes a remote image or index from src to dst. +// THIS API IS EXPERIMENTAL AND SUBJECT TO CHANGE WITHOUT WARNING. +func Optimize(src, dst string, prioritize []string, opt ...Option) error { + pset := newStringSet(prioritize) + o := makeOptions(opt...) + srcRef, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", src, err) + } + + dstRef, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference for %q: %w", dst, err) + } + + logs.Progress.Printf("Optimizing from %v to %v", srcRef, dstRef) + desc, err := remote.Get(srcRef, o.Remote...) + if err != nil { + return fmt.Errorf("fetching %q: %w", src, err) + } + + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + // Handle indexes separately. + if o.Platform != nil { + // If platform is explicitly set, don't optimize the whole index, just the appropriate image. + if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize image: %w", err) + } + } else { + if err := optimizeAndPushIndex(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize index: %w", err) + } + } + + case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: + return errors.New("docker schema 1 images are not supported") + + default: + // Assume anything else is an image, since some registries don't set mediaTypes properly. + if err := optimizeAndPushImage(desc, dstRef, pset, o); err != nil { + return fmt.Errorf("failed to optimize image: %w", err) + } + } + + return nil +} + +func optimizeAndPushImage(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { + img, err := desc.Image() + if err != nil { + return err + } + + missing, oimg, err := optimizeImage(img, prioritize) + if err != nil { + return err + } + + if len(missing) > 0 { + return fmt.Errorf("the following prioritized files were missing from image: %v", missing.List()) + } + + return remote.Write(dstRef, oimg, o.Remote...) +} + +func optimizeImage(img v1.Image, prioritize stringSet) (stringSet, v1.Image, error) { + cfg, err := img.ConfigFile() + if err != nil { + return nil, nil, err + } + ocfg := cfg.DeepCopy() + ocfg.History = nil + ocfg.RootFS.DiffIDs = nil + + oimg, err := mutate.ConfigFile(empty.Image, ocfg) + if err != nil { + return nil, nil, err + } + + layers, err := img.Layers() + if err != nil { + return nil, nil, err + } + + missingFromImage := newStringSet(prioritize.List()) + olayers := make([]mutate.Addendum, 0, len(layers)) + for _, layer := range layers { + missingFromLayer := []string{} + olayer, err := tarball.LayerFromOpener(layer.Uncompressed, + tarball.WithEstargz, + tarball.WithEstargzOptions( + estargz.WithPrioritizedFiles(prioritize.List()), + estargz.WithAllowPrioritizeNotFound(&missingFromLayer), + )) + if err != nil { + return nil, nil, err + } + missingFromImage = missingFromImage.Intersection(newStringSet(missingFromLayer)) + + olayers = append(olayers, mutate.Addendum{ + Layer: olayer, + MediaType: types.DockerLayer, + }) + } + + oimg, err = mutate.Append(oimg, olayers...) + if err != nil { + return nil, nil, err + } + return missingFromImage, oimg, nil +} + +func optimizeAndPushIndex(desc *remote.Descriptor, dstRef name.Reference, prioritize stringSet, o Options) error { + idx, err := desc.ImageIndex() + if err != nil { + return err + } + + missing, oidx, err := optimizeIndex(idx, prioritize) + if err != nil { + return err + } + + if len(missing) > 0 { + return fmt.Errorf("the following prioritized files were missing from all images: %v", missing.List()) + } + + return remote.WriteIndex(dstRef, oidx, o.Remote...) +} + +func optimizeIndex(idx v1.ImageIndex, prioritize stringSet) (stringSet, v1.ImageIndex, error) { + im, err := idx.IndexManifest() + if err != nil { + return nil, nil, err + } + + missingFromIndex := newStringSet(prioritize.List()) + + // Build an image for each child from the base and append it to a new index to produce the result. + adds := make([]mutate.IndexAddendum, 0, len(im.Manifests)) + for _, desc := range im.Manifests { + img, err := idx.Image(desc.Digest) + if err != nil { + return nil, nil, err + } + + missingFromImage, oimg, err := optimizeImage(img, prioritize) + if err != nil { + return nil, nil, err + } + missingFromIndex = missingFromIndex.Intersection(missingFromImage) + adds = append(adds, mutate.IndexAddendum{ + Add: oimg, + Descriptor: v1.Descriptor{ + URLs: desc.URLs, + MediaType: desc.MediaType, + Annotations: desc.Annotations, + Platform: desc.Platform, + }, + }) + } + + idxType, err := idx.MediaType() + if err != nil { + return nil, nil, err + } + + return missingFromIndex, mutate.IndexMediaType(mutate.AppendManifests(empty.Index, adds...), idxType), nil +} + +type stringSet map[string]struct{} + +func newStringSet(in []string) stringSet { + ss := stringSet{} + for _, s := range in { + ss[s] = struct{}{} + } + return ss +} + +func (s stringSet) List() []string { + result := make([]string, 0, len(s)) + for k := range s { + result = append(result, k) + } + return result +} + +func (s stringSet) Intersection(rhs stringSet) stringSet { + // To appease ST1016 + lhs := s + + // Make sure len(lhs) >= len(rhs) + if len(lhs) < len(rhs) { + return rhs.Intersection(lhs) + } + + result := stringSet{} + for k := range lhs { + if _, ok := rhs[k]; ok { + result[k] = struct{}{} + } + } + return result +} diff --git a/pkg/crane/optimize_test.go b/pkg/crane/optimize_test.go new file mode 100644 index 0000000..11aaf57 --- /dev/null +++ b/pkg/crane/optimize_test.go @@ -0,0 +1,179 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "net/http/httptest" + "net/url" + "path" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func TestStringSet(t *testing.T) { + for _, tc := range []struct { + lhs []string + rhs []string + result []string + }{{ + lhs: []string{}, + rhs: []string{}, + result: []string{}, + }, { + lhs: []string{"a"}, + rhs: []string{}, + result: []string{}, + }, { + lhs: []string{}, + rhs: []string{"a"}, + result: []string{}, + }, { + lhs: []string{"a", "b", "c"}, + rhs: []string{"a", "b", "c"}, + result: []string{"a", "b", "c"}, + }, { + lhs: []string{"a", "b"}, + rhs: []string{"a"}, + result: []string{"a"}, + }, { + lhs: []string{"a"}, + rhs: []string{"a", "b"}, + result: []string{"a"}, + }} { + got := newStringSet(tc.lhs).Intersection(newStringSet(tc.rhs)) + want := newStringSet(tc.result) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("%v.intersect(%v) (-want +got): %s", tc.lhs, tc.rhs, diff) + } + + less := func(a, b string) bool { + return strings.Compare(a, b) <= -1 + } + if diff := cmp.Diff(tc.result, got.List(), cmpopts.SortSlices(less)); diff != "" { + t.Errorf("%v.List() (-want +got): = %v", tc.result, diff) + } + } +} + +func TestOptimize(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + imgs := []mutate.IndexAddendum{} + for _, plat := range []string{ + "linux/amd64", + "linux/arm", + } { + img, err := Image(map[string][]byte{ + "unimportant": []byte(strings.Repeat("deadbeef", 128)), + "important": []byte(`abc`), + "platform.txt": []byte(plat), + }) + if err != nil { + t.Fatal(err) + } + parts := strings.Split(plat, "/") + imgs = append(imgs, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + Platform: &v1.Platform{ + OS: parts[0], + Architecture: parts[1], + }, + }, + }) + } + + idx := mutate.AppendManifests(empty.Index, imgs...) + + slow := path.Join(u.Host, "slow") + fast := path.Join(u.Host, "fast") + + ref, err := name.ParseReference(slow) + if err != nil { + t.Fatal(err) + } + + if err := remote.WriteIndex(ref, idx); err != nil { + t.Fatal(err) + } + + if err := Optimize(slow, fast, []string{"important"}); err != nil { + t.Fatal(err) + } + + if err := Optimize(slow, fast, []string{"important"}, WithPlatform(imgs[1].Platform)); err != nil { + t.Fatal(err) + } + + // Compare optimize WithPlatform path to optimizing just an image. + got, err := Digest(fast) + if err != nil { + t.Fatal(err) + } + + dig, err := Digest(slow, WithPlatform(imgs[1].Platform)) + if err != nil { + t.Fatal(err) + } + + slowImgRef := slow + "@" + dig + if err := Optimize(slowImgRef, fast, []string{"important"}, WithPlatform(imgs[1].Platform)); err != nil { + t.Fatal(err) + } + + want, err := Digest(fast) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Errorf("Optimize(WithPlatform) != Optimize(bydigest): %q != %q", got, want) + } + + for i, ref := range []string{slow, slow, slowImgRef} { + opts := []Option{} + // Silly, but use WithPlatform to get some more coverage. + if i != 0 { + opts = []Option{WithPlatform(imgs[1].Platform)} + } + dig, err := Digest(ref, opts...) + if err != nil { + t.Errorf("Digest(%q): %v", ref, err) + continue + } + // Make sure we fail if there's a missing file in the optimize set + // Use the image digest because it's ~impossible to exist in img. + if err := Optimize(ref, fast, []string{dig}, opts...); err == nil { + t.Errorf("Optimize(%q, prioritize=%q): got nil, want err", ref, dig) + } else if !strings.Contains(err.Error(), dig) { + // Make sure this contains the missing file (dig) + t.Errorf("Optimize(%q) error should contain %q, got: %v", ref, dig, err) + } + } +} diff --git a/pkg/crane/options.go b/pkg/crane/options.go new file mode 100644 index 0000000..5d2e0e4 --- /dev/null +++ b/pkg/crane/options.go @@ -0,0 +1,149 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "context" + "crypto/tls" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Options hold the options that crane uses when calling other packages. +type Options struct { + Name []name.Option + Remote []remote.Option + Platform *v1.Platform + Keychain authn.Keychain + + transport http.RoundTripper + insecure bool +} + +// GetOptions exposes the underlying []remote.Option, []name.Option, and +// platform, based on the passed Option. Generally, you shouldn't need to use +// this unless you've painted yourself into a dependency corner as we have +// with the crane and gcrane cli packages. +func GetOptions(opts ...Option) Options { + return makeOptions(opts...) +} + +func makeOptions(opts ...Option) Options { + opt := Options{ + Remote: []remote.Option{ + remote.WithAuthFromKeychain(authn.DefaultKeychain), + }, + Keychain: authn.DefaultKeychain, + } + + for _, o := range opts { + o(&opt) + } + + // Allow for untrusted certificates if the user + // passed Insecure but no custom transport. + if opt.insecure && opt.transport == nil { + transport := remote.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, //nolint: gosec + } + + WithTransport(transport)(&opt) + } + + return opt +} + +// Option is a functional option for crane. +type Option func(*Options) + +// WithTransport is a functional option for overriding the default transport +// for remote operations. Setting a transport will override the Insecure option's +// configuration allowing for image registries to use untrusted certificates. +func WithTransport(t http.RoundTripper) Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithTransport(t)) + o.transport = t + } +} + +// Insecure is an Option that allows image references to be fetched without TLS. +// This will also allow for untrusted (e.g. self-signed) certificates in cases where +// the default transport is used (i.e. when WithTransport is not used). +func Insecure(o *Options) { + o.Name = append(o.Name, name.Insecure) + o.insecure = true +} + +// WithPlatform is an Option to specify the platform. +func WithPlatform(platform *v1.Platform) Option { + return func(o *Options) { + if platform != nil { + o.Remote = append(o.Remote, remote.WithPlatform(*platform)) + } + o.Platform = platform + } +} + +// WithAuthFromKeychain is a functional option for overriding the default +// authenticator for remote operations, using an authn.Keychain to find +// credentials. +// +// By default, crane will use authn.DefaultKeychain. +func WithAuthFromKeychain(keys authn.Keychain) Option { + return func(o *Options) { + // Replace the default keychain at position 0. + o.Remote[0] = remote.WithAuthFromKeychain(keys) + o.Keychain = keys + } +} + +// WithAuth is a functional option for overriding the default authenticator +// for remote operations. +// +// By default, crane will use authn.DefaultKeychain. +func WithAuth(auth authn.Authenticator) Option { + return func(o *Options) { + // Replace the default keychain at position 0. + o.Remote[0] = remote.WithAuth(auth) + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. +func WithUserAgent(ua string) Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithUserAgent(ua)) + } +} + +// WithNondistributable is an option that allows pushing non-distributable +// layers. +func WithNondistributable() Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithNondistributable) + } +} + +// WithContext is a functional option for setting the context. +func WithContext(ctx context.Context) Option { + return func(o *Options) { + o.Remote = append(o.Remote, remote.WithContext(ctx)) + } +} diff --git a/pkg/crane/options_test.go b/pkg/crane/options_test.go new file mode 100644 index 0000000..98d7396 --- /dev/null +++ b/pkg/crane/options_test.go @@ -0,0 +1,58 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "errors" + "net/http" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func TestInsecureOptionTracking(t *testing.T) { + want := true + opts := GetOptions(Insecure) + + if got := opts.insecure; got != want { + t.Errorf("got %t\nwant: %t", got, want) + } +} + +func TestTransportSetting(t *testing.T) { + opts := GetOptions(WithTransport(remote.DefaultTransport)) + + if opts.transport == nil { + t.Error("expected crane transport to be set when user passes WithTransport") + } +} + +func TestInsecureTransport(t *testing.T) { + want := true + opts := GetOptions(Insecure) + var transport *http.Transport + var ok bool + if transport, ok = opts.transport.(*http.Transport); !ok { + t.Fatal("Unable to successfully assert default transport") + } + + if transport.TLSClientConfig == nil { + t.Fatal(errors.New("TLSClientConfig was nil and should be set")) + } + + if got := transport.TLSClientConfig.InsecureSkipVerify; got != want { + t.Errorf("got: %t\nwant: %t", got, want) + } +} diff --git a/pkg/crane/pull.go b/pkg/crane/pull.go new file mode 100644 index 0000000..7e6e5b7 --- /dev/null +++ b/pkg/crane/pull.go @@ -0,0 +1,142 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + "os" + + legacy "github.com/google/go-containerregistry/pkg/legacy/tarball" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Tag applied to images that were pulled by digest. This denotes that the +// image was (probably) never tagged with this, but lets us avoid applying the +// ":latest" tag which might be misleading. +const iWasADigestTag = "i-was-a-digest" + +// Pull returns a v1.Image of the remote image src. +func Pull(src string, opt ...Option) (v1.Image, error) { + o := makeOptions(opt...) + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing reference %q: %w", src, err) + } + + return remote.Image(ref, o.Remote...) +} + +// Save writes the v1.Image img as a tarball at path with tag src. +func Save(img v1.Image, src, path string) error { + imgMap := map[string]v1.Image{src: img} + return MultiSave(imgMap, path) +} + +// MultiSave writes collection of v1.Image img with tag as a tarball. +func MultiSave(imgMap map[string]v1.Image, path string, opt ...Option) error { + o := makeOptions(opt...) + tagToImage := map[name.Tag]v1.Image{} + + for src, img := range imgMap { + ref, err := name.ParseReference(src, o.Name...) + if err != nil { + return fmt.Errorf("parsing ref %q: %w", src, err) + } + + // WriteToFile wants a tag to write to the tarball, but we might have + // been given a digest. + // If the original ref was a tag, use that. Otherwise, if it was a + // digest, tag the image with :i-was-a-digest instead. + tag, ok := ref.(name.Tag) + if !ok { + d, ok := ref.(name.Digest) + if !ok { + return fmt.Errorf("ref wasn't a tag or digest") + } + tag = d.Repository.Tag(iWasADigestTag) + } + tagToImage[tag] = img + } + // no progress channel (for now) + return tarball.MultiWriteToFile(path, tagToImage) +} + +// PullLayer returns the given layer from a registry. +func PullLayer(ref string, opt ...Option) (v1.Layer, error) { + o := makeOptions(opt...) + digest, err := name.NewDigest(ref, o.Name...) + if err != nil { + return nil, err + } + + return remote.Layer(digest, o.Remote...) +} + +// SaveLegacy writes the v1.Image img as a legacy tarball at path with tag src. +func SaveLegacy(img v1.Image, src, path string) error { + imgMap := map[string]v1.Image{src: img} + return MultiSave(imgMap, path) +} + +// MultiSaveLegacy writes collection of v1.Image img with tag as a legacy tarball. +func MultiSaveLegacy(imgMap map[string]v1.Image, path string) error { + refToImage := map[name.Reference]v1.Image{} + + for src, img := range imgMap { + ref, err := name.ParseReference(src) + if err != nil { + return fmt.Errorf("parsing ref %q: %w", src, err) + } + refToImage[ref] = img + } + + w, err := os.Create(path) + if err != nil { + return err + } + defer w.Close() + + return legacy.MultiWrite(refToImage, w) +} + +// SaveOCI writes the v1.Image img as an OCI Image Layout at path. If a layout +// already exists at that path, it will add the image to the index. +func SaveOCI(img v1.Image, path string) error { + imgMap := map[string]v1.Image{"": img} + return MultiSaveOCI(imgMap, path) +} + +// MultiSaveOCI writes collection of v1.Image img as an OCI Image Layout at path. If a layout +// already exists at that path, it will add the image to the index. +func MultiSaveOCI(imgMap map[string]v1.Image, path string) error { + p, err := layout.FromPath(path) + if err != nil { + p, err = layout.Write(path, empty.Index) + if err != nil { + return err + } + } + for _, img := range imgMap { + if err = p.AppendImage(img); err != nil { + return err + } + } + return nil +} diff --git a/pkg/crane/push.go b/pkg/crane/push.go new file mode 100644 index 0000000..6d1fbd6 --- /dev/null +++ b/pkg/crane/push.go @@ -0,0 +1,65 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Load reads the tarball at path as a v1.Image. +func Load(path string, opt ...Option) (v1.Image, error) { + return LoadTag(path, "") +} + +// LoadTag reads a tag from the tarball at path as a v1.Image. +// If tag is "", will attempt to read the tarball as a single image. +func LoadTag(path, tag string, opt ...Option) (v1.Image, error) { + if tag == "" { + return tarball.ImageFromPath(path, nil) + } + + o := makeOptions(opt...) + t, err := name.NewTag(tag, o.Name...) + if err != nil { + return nil, fmt.Errorf("parsing tag %q: %w", tag, err) + } + return tarball.ImageFromPath(path, &t) +} + +// Push pushes the v1.Image img to a registry as dst. +func Push(img v1.Image, dst string, opt ...Option) error { + o := makeOptions(opt...) + tag, err := name.ParseReference(dst, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", dst, err) + } + return remote.Write(tag, img, o.Remote...) +} + +// Upload pushes the v1.Layer to a given repo. +func Upload(layer v1.Layer, repo string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.NewRepository(repo, o.Name...) + if err != nil { + return fmt.Errorf("parsing repo %q: %w", repo, err) + } + + return remote.WriteLayer(ref, layer, o.Remote...) +} diff --git a/pkg/crane/tag.go b/pkg/crane/tag.go new file mode 100644 index 0000000..13bc395 --- /dev/null +++ b/pkg/crane/tag.go @@ -0,0 +1,39 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package crane + +import ( + "fmt" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Tag adds tag to the remote img. +func Tag(img, tag string, opt ...Option) error { + o := makeOptions(opt...) + ref, err := name.ParseReference(img, o.Name...) + if err != nil { + return fmt.Errorf("parsing reference %q: %w", img, err) + } + desc, err := remote.Get(ref, o.Remote...) + if err != nil { + return fmt.Errorf("fetching %q: %w", img, err) + } + + dst := ref.Context().Tag(tag) + + return remote.Tag(dst, desc, o.Remote...) +} diff --git a/pkg/crane/testdata/content.tar b/pkg/crane/testdata/content.tar Binary files differnew file mode 100755 index 0000000..55f4d1d --- /dev/null +++ b/pkg/crane/testdata/content.tar diff --git a/pkg/gcrane/copy.go b/pkg/gcrane/copy.go new file mode 100644 index 0000000..a3362c4 --- /dev/null +++ b/pkg/gcrane/copy.go @@ -0,0 +1,347 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcrane + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/google/go-containerregistry/internal/retry" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "golang.org/x/sync/errgroup" +) + +// Keychain tries to use google-specific credential sources, falling back to +// the DefaultKeychain (config-file based). +var Keychain = authn.NewMultiKeychain(google.Keychain, authn.DefaultKeychain) + +// GCRBackoff returns a retry.Backoff that is suitable for use with gcr.io. +// +// These numbers are based on GCR's posted quotas: +// https://cloud.google.com/container-registry/quotas +// - 30k requests per 10 minutes. +// - 500k requests per 24 hours. +// +// On error, we will wait for: +// - 6 seconds (in case of very short term 429s from GCS), then +// - 1 minute (in case of temporary network issues), then +// - 10 minutes (to get around GCR 10 minute quotas), then fail. +// +// TODO: In theory, we could keep retrying until the next day to get around the 500k limit. +func GCRBackoff() retry.Backoff { + return retry.Backoff{ + Duration: 6 * time.Second, + Factor: 10.0, + Jitter: 0.1, + Steps: 3, + Cap: 1 * time.Hour, + } +} + +// Copy copies a remote image or index from src to dst. +func Copy(src, dst string, opts ...Option) error { + o := makeOptions(opts...) + // Just reuse crane's copy logic with gcrane's credential logic. + return crane.Copy(src, dst, o.crane...) +} + +// CopyRepository copies everything from the src GCR repository to the +// dst GCR repository. +func CopyRepository(ctx context.Context, src, dst string, opts ...Option) error { + o := makeOptions(opts...) + return recursiveCopy(ctx, src, dst, o) +} + +type task struct { + digest string + manifest google.ManifestInfo + oldRepo name.Repository + newRepo name.Repository +} + +type copier struct { + srcRepo name.Repository + dstRepo name.Repository + + tasks chan task + opt *options +} + +func newCopier(src, dst string, o *options) (*copier, error) { + srcRepo, err := name.NewRepository(src) + if err != nil { + return nil, fmt.Errorf("parsing repo %q: %w", src, err) + } + + dstRepo, err := name.NewRepository(dst) + if err != nil { + return nil, fmt.Errorf("parsing repo %q: %w", dst, err) + } + + // A queue of size 2*jobs should keep each goroutine busy. + tasks := make(chan task, o.jobs*2) + + return &copier{srcRepo, dstRepo, tasks, o}, nil +} + +// recursiveCopy copies images from repo src to repo dst. +func recursiveCopy(ctx context.Context, src, dst string, o *options) error { + c, err := newCopier(src, dst, o) + if err != nil { + return err + } + + g, ctx := errgroup.WithContext(ctx) + walkFn := func(repo name.Repository, tags *google.Tags, err error) error { + if err != nil { + logs.Warn.Printf("failed walkFn for repo %s: %v", repo, err) + // If we hit an error when listing the repo, try re-listing with backoff. + if err := backoffErrors(GCRBackoff(), func() error { + tags, err = google.List(repo, o.google...) + return err + }); err != nil { + return fmt.Errorf("failed List for repo %s: %w", repo, err) + } + } + + // If we hit an error when trying to diff the repo, re-diff with backoff. + if err := backoffErrors(GCRBackoff(), func() error { + return c.copyRepo(ctx, repo, tags) + }); err != nil { + return fmt.Errorf("failed to copy repo %q: %w", repo, err) + } + + return nil + } + + // Start walking the repo, enqueuing items in c.tasks. + g.Go(func() error { + defer close(c.tasks) + if err := google.Walk(c.srcRepo, walkFn, o.google...); err != nil { + return fmt.Errorf("failed to Walk: %w", err) + } + return nil + }) + + // Pull items off of c.tasks and copy the images. + for i := 0; i < o.jobs; i++ { + g.Go(func() error { + for task := range c.tasks { + // If we hit an error when trying to copy the images, + // retry with backoff. + if err := backoffErrors(GCRBackoff(), func() error { + return c.copyImages(ctx, task) + }); err != nil { + return fmt.Errorf("failed to copy %q: %w", task.digest, err) + } + } + return nil + }) + } + + return g.Wait() +} + +// copyRepo figures out the name for our destination repo (newRepo), lists the +// contents of newRepo, calculates the diff of what needs to be copied, then +// starts a goroutine to copy each image we need, and waits for them to finish. +func (c *copier) copyRepo(ctx context.Context, oldRepo name.Repository, tags *google.Tags) error { + newRepo, err := c.rename(oldRepo) + if err != nil { + return fmt.Errorf("rename failed: %w", err) + } + + // Figure out what we actually need to copy. + want := tags.Manifests + have := make(map[string]google.ManifestInfo) + haveTags, err := google.List(newRepo, c.opt.google...) + if err != nil { + if !hasStatusCode(err, http.StatusNotFound) { + return err + } + // This is a 404 code, so we just need to copy everything. + logs.Warn.Printf("failed to list %s: %v", newRepo, err) + } else { + have = haveTags.Manifests + } + need := diffImages(want, have) + + // Queue up every image as a task. + for digest, manifest := range need { + t := task{ + digest: digest, + manifest: manifest, + oldRepo: oldRepo, + newRepo: newRepo, + } + select { + case c.tasks <- t: + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + +// copyImages starts a goroutine for each tag that points to the image +// oldRepo@digest, or just copies the image by digest if there are no tags. +func (c *copier) copyImages(_ context.Context, t task) error { + // We only have to explicitly copy by digest if there are no tags pointing to this manifest. + if len(t.manifest.Tags) == 0 { + srcImg := fmt.Sprintf("%s@%s", t.oldRepo, t.digest) + dstImg := fmt.Sprintf("%s@%s", t.newRepo, t.digest) + + return crane.Copy(srcImg, dstImg, c.opt.crane...) + } + + // We only need to push the whole image once. + tag := t.manifest.Tags[0] + srcImg := fmt.Sprintf("%s:%s", t.oldRepo, tag) + dstImg := fmt.Sprintf("%s:%s", t.newRepo, tag) + + if err := crane.Copy(srcImg, dstImg, c.opt.crane...); err != nil { + return err + } + + if len(t.manifest.Tags) <= 1 { + // If there's only one tag, we're done. + return nil + } + + // Add the rest of the tags. + srcRef, err := name.ParseReference(srcImg) + if err != nil { + return err + } + desc, err := remote.Get(srcRef, c.opt.remote...) + if err != nil { + return err + } + + for _, tag := range t.manifest.Tags[1:] { + dstImg := t.newRepo.Tag(tag) + + if err := remote.Tag(dstImg, desc, c.opt.remote...); err != nil { + return err + } + } + + return nil +} + +// Retry temporary errors, 429, and 500+ with backoff. +func backoffErrors(bo retry.Backoff, f func() error) error { + p := func(err error) bool { + b := retry.IsTemporary(err) || hasStatusCode(err, http.StatusTooManyRequests) || isServerError(err) + if b { + logs.Warn.Printf("Retrying %v", err) + } + return b + } + return retry.Retry(f, p, bo) +} + +func hasStatusCode(err error, code int) bool { + if err == nil { + return false + } + var terr *transport.Error + if errors.As(err, &terr) { + if terr.StatusCode == code { + return true + } + } + return false +} + +func isServerError(err error) bool { + if err == nil { + return false + } + var terr *transport.Error + if errors.As(err, &terr) { + return terr.StatusCode >= 500 + } + return false +} + +// rename figures out the name of the new repository to copy to, e.g.: +// +// $ gcrane cp -r gcr.io/foo gcr.io/baz +// +// rename("gcr.io/foo/bar") == "gcr.io/baz/bar" +func (c *copier) rename(repo name.Repository) (name.Repository, error) { + replaced := strings.Replace(repo.String(), c.srcRepo.String(), c.dstRepo.String(), 1) + return name.NewRepository(replaced, name.StrictValidation) +} + +// diffImages returns a map of digests to google.ManifestInfos for images or +// tags that are present in "want" but not in "have". +func diffImages(want, have map[string]google.ManifestInfo) map[string]google.ManifestInfo { + need := make(map[string]google.ManifestInfo) + + for digest, wantManifest := range want { + if haveManifest, ok := have[digest]; !ok { + // Missing the whole image, we need to copy everything. + need[digest] = wantManifest + } else { + missingTags := subtractStringLists(wantManifest.Tags, haveManifest.Tags) + if len(missingTags) == 0 { + continue + } + + // Missing just some tags, add the ones we need to copy. + todo := wantManifest + todo.Tags = missingTags + need[digest] = todo + } + } + + return need +} + +// subtractStringLists returns a list of strings that are in minuend and not +// in subtrahend; order is unimportant. +func subtractStringLists(minuend, subtrahend []string) []string { + bSet := toStringSet(subtrahend) + difference := []string{} + + for _, a := range minuend { + if _, ok := bSet[a]; !ok { + difference = append(difference, a) + } + } + + return difference +} + +func toStringSet(slice []string) map[string]struct{} { + set := make(map[string]struct{}, len(slice)) + for _, s := range slice { + set[s] = struct{}{} + } + return set +} diff --git a/pkg/gcrane/copy_test.go b/pkg/gcrane/copy_test.go new file mode 100644 index 0000000..e50564a --- /dev/null +++ b/pkg/gcrane/copy_test.go @@ -0,0 +1,428 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcrane + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/internal/retry" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type fakeXCR struct { + h http.Handler + repos map[string]google.Tags + t *testing.T +} + +func (xcr *fakeXCR) ServeHTTP(w http.ResponseWriter, r *http.Request) { + xcr.t.Logf("%s %s", r.Method, r.URL) + if strings.HasPrefix(r.URL.Path, "/v2/") && strings.HasSuffix(r.URL.Path, "/tags/list") { + repo := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v2/"), "/tags/list") + if tags, ok := xcr.repos[repo]; !ok { + w.WriteHeader(http.StatusNotFound) + } else { + xcr.t.Logf("%+v", tags) + if err := json.NewEncoder(w).Encode(tags); err != nil { + xcr.t.Fatal(err) + } + } + } else { + xcr.h.ServeHTTP(w, r) + } +} + +func newFakeXCR(t *testing.T) *fakeXCR { + h := registry.New() + return &fakeXCR{h: h, t: t} +} + +func (xcr *fakeXCR) setRefs(stuff map[name.Reference]partial.Describable) error { + repos := make(map[string]google.Tags) + + for ref, thing := range stuff { + repo := ref.Context().RepositoryStr() + tags, ok := repos[repo] + if !ok { + tags = google.Tags{ + Name: repo, + Children: []string{}, + } + } + + // Populate the "child" field. + for parentPath := repo; parentPath != "."; parentPath = path.Dir(parentPath) { + child, parent := path.Base(parentPath), path.Dir(parentPath) + tags, ok := repos[parent] + if !ok { + tags = google.Tags{} + } + for _, c := range repos[parent].Children { + if c == child { + break + } + } + tags.Children = append(tags.Children, child) + repos[parent] = tags + } + + // Populate the "manifests" and "tags" field. + d, err := thing.Digest() + if err != nil { + return err + } + mt, err := thing.MediaType() + if err != nil { + return err + } + if tags.Manifests == nil { + tags.Manifests = make(map[string]google.ManifestInfo) + } + mi, ok := tags.Manifests[d.String()] + if !ok { + mi = google.ManifestInfo{ + MediaType: string(mt), + Tags: []string{}, + } + } + if tag, ok := ref.(name.Tag); ok { + tags.Tags = append(tags.Tags, tag.Identifier()) + mi.Tags = append(mi.Tags, tag.Identifier()) + } + tags.Manifests[d.String()] = mi + repos[repo] = tags + } + xcr.repos = repos + return nil +} + +func TestCopy(t *testing.T) { + logs.Warn.SetOutput(os.Stderr) + xcr := newFakeXCR(t) + s := httptest.NewServer(xcr) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + defer s.Close() + src := path.Join(u.Host, "test/gcrane") + dst := path.Join(u.Host, "test/gcrane/copy") + + oneTag, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + twoTags, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + noTags, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + + latestRef, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + oneTagRef := latestRef.Context().Tag("bar") + + d, err := noTags.Digest() + if err != nil { + t.Fatal(err) + } + noTagsRef := latestRef.Context().Digest(d.String()) + fooRef := latestRef.Context().Tag("foo") + + // Populate this after we create it so we know the hostname. + if err := xcr.setRefs(map[name.Reference]partial.Describable{ + oneTagRef: oneTag, + latestRef: twoTags, + fooRef: twoTags, + noTagsRef: noTags, + }); err != nil { + t.Fatal(err) + } + + if err := remote.Write(latestRef, twoTags); err != nil { + t.Fatal(err) + } + if err := remote.Write(fooRef, twoTags); err != nil { + t.Fatal(err) + } + if err := remote.Write(oneTagRef, oneTag); err != nil { + t.Fatal(err) + } + if err := remote.Write(noTagsRef, noTags); err != nil { + t.Fatal(err) + } + + if err := Copy(src, dst); err != nil { + t.Fatal(err) + } + + if err := CopyRepository(context.Background(), src, dst); err != nil { + t.Fatal(err) + } +} + +func TestRename(t *testing.T) { + c := copier{ + srcRepo: name.MustParseReference("registry.example.com/foo").Context(), + dstRepo: name.MustParseReference("registry.example.com/bar").Context(), + } + + got, err := c.rename(name.MustParseReference("registry.example.com/foo/sub/repo").Context()) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + want := name.MustParseReference("registry.example.com/bar/sub/repo").Context() + + if want.String() != got.String() { + t.Errorf("%s != %s", want, got) + } +} + +func TestSubtractStringLists(t *testing.T) { + cases := []struct { + minuend []string + subtrahend []string + result []string + }{{ + minuend: []string{"a", "b", "c"}, + subtrahend: []string{"a"}, + result: []string{"b", "c"}, + }, { + minuend: []string{"a", "a", "a"}, + subtrahend: []string{"a", "b"}, + result: []string{}, + }, { + minuend: []string{}, + subtrahend: []string{"a", "b"}, + result: []string{}, + }, { + minuend: []string{"a", "b"}, + subtrahend: []string{}, + result: []string{"a", "b"}, + }} + + for _, tc := range cases { + want, got := tc.result, subtractStringLists(tc.minuend, tc.subtrahend) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("subtracting string lists: %v - %v: (-want +got)\n%s", tc.minuend, tc.subtrahend, diff) + } + } +} + +func TestDiffImages(t *testing.T) { + cases := []struct { + want map[string]google.ManifestInfo + have map[string]google.ManifestInfo + need map[string]google.ManifestInfo + }{{ + // Have everything we need. + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c"}, + }, + }, + have: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c"}, + }, + }, + need: map[string]google.ManifestInfo{}, + }, { + // Missing image a. + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{}, + need: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + }, { + // Missing tags "b" and "d" + want: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"c"}, + }, + }, + need: map[string]google.ManifestInfo{ + "a": { + Tags: []string{"b", "d"}, + }, + }, + }, { + // Make sure all properties get copied over. + want: map[string]google.ManifestInfo{ + "a": { + Size: 123, + MediaType: string(types.DockerManifestSchema2), + Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC), + Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC), + Tags: []string{"b", "c", "d"}, + }, + }, + have: map[string]google.ManifestInfo{}, + need: map[string]google.ManifestInfo{ + "a": { + Size: 123, + MediaType: string(types.DockerManifestSchema2), + Created: time.Date(1992, time.January, 7, 6, 40, 00, 5e8, time.UTC), + Uploaded: time.Date(2018, time.November, 29, 4, 13, 30, 5e8, time.UTC), + Tags: []string{"b", "c", "d"}, + }, + }, + }} + + for _, tc := range cases { + want, got := tc.need, diffImages(tc.want, tc.have) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("diffing images: %v - %v: (-want +got)\n%s", tc.want, tc.have, diff) + } + } +} + +// Test that our backoff works the way we expect. +func TestBackoff(t *testing.T) { + backoff := GCRBackoff() + + if d := backoff.Step(); d > 10*time.Second { + t.Errorf("Duration too long: %v", d) + } + if d := backoff.Step(); d > 100*time.Second { + t.Errorf("Duration too long: %v", d) + } + if d := backoff.Step(); d > 1000*time.Second { + t.Errorf("Duration too long: %v", d) + } + if s := backoff.Steps; s != 0 { + t.Errorf("backoff.Steps should be 0, got %d", s) + } +} + +func TestErrors(t *testing.T) { + if hasStatusCode(nil, http.StatusOK) { + t.Fatal("nil error should not have any status code") + } + if !hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusOK) { + t.Fatal("200 should be 200") + } + if hasStatusCode(&transport.Error{StatusCode: http.StatusOK}, http.StatusNotFound) { + t.Fatal("200 should not be 404") + } + + if isServerError(nil) { + t.Fatal("nil should not be server error") + } + if isServerError(fmt.Errorf("i am a string")) { + t.Fatal("string should not be server error") + } + if !isServerError(&transport.Error{StatusCode: http.StatusServiceUnavailable}) { + t.Fatal("503 should be server error") + } + if isServerError(&transport.Error{StatusCode: http.StatusTooManyRequests}) { + t.Fatal("429 should not be server error") + } +} + +func TestRetryErrors(t *testing.T) { + // We log a warning during retries, so we can tell if something retried by checking logs.Warn. + var b bytes.Buffer + logs.Warn.SetOutput(&b) + + err := backoffErrors(retry.Backoff{ + Duration: 1 * time.Millisecond, + Steps: 3, + }, func() error { + return &transport.Error{StatusCode: http.StatusTooManyRequests} + }) + + if err == nil { + t.Fatal("backoffErrors should return internal err, got nil") + } + var terr *transport.Error + if !errors.As(err, &terr) { + t.Fatalf("backoffErrors should return internal err, got different error: %v", err) + } else if terr.StatusCode != http.StatusTooManyRequests { + t.Fatalf("backoffErrors should return internal err, got different status code: %v", terr.StatusCode) + } + + if b.Len() == 0 { + t.Fatal("backoffErrors didn't log to logs.Warn") + } +} + +func TestBadInputs(t *testing.T) { + t.Parallel() + invalid := "@@@@@@" + + // Create a valid image reference that will fail with not found. + s := httptest.NewServer(http.NotFoundHandler()) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + valid404 := fmt.Sprintf("%s/some/image", u.Host) + + ctx := context.Background() + + for _, tc := range []struct { + desc string + err error + }{ + {"Copy(invalid, invalid)", Copy(invalid, invalid)}, + {"Copy(404, invalid)", Copy(valid404, invalid)}, + {"Copy(404, 404)", Copy(valid404, valid404)}, + {"CopyRepository(invalid, invalid)", CopyRepository(ctx, invalid, invalid)}, + {"CopyRepository(404, invalid)", CopyRepository(ctx, valid404, invalid)}, + {"CopyRepository(404, 404)", CopyRepository(ctx, valid404, valid404, WithJobs(1))}, + } { + if tc.err == nil { + t.Errorf("%s: expected err, got nil", tc.desc) + } + } +} diff --git a/pkg/gcrane/doc.go b/pkg/gcrane/doc.go new file mode 100644 index 0000000..63a1bbb --- /dev/null +++ b/pkg/gcrane/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gcrane holds libraries used to implement the gcrane CLI. +package gcrane diff --git a/pkg/gcrane/options.go b/pkg/gcrane/options.go new file mode 100644 index 0000000..9d34c7d --- /dev/null +++ b/pkg/gcrane/options.go @@ -0,0 +1,122 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcrane + +import ( + "context" + "net/http" + "runtime" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/v1/google" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +// Option is a functional option for gcrane operations. +type Option func(*options) + +type options struct { + jobs int + remote []remote.Option + google []google.Option + crane []crane.Option +} + +func makeOptions(opts ...Option) *options { + o := &options{ + jobs: runtime.GOMAXPROCS(0), + remote: []remote.Option{ + remote.WithAuthFromKeychain(Keychain), + }, + google: []google.Option{ + google.WithAuthFromKeychain(Keychain), + }, + crane: []crane.Option{ + crane.WithAuthFromKeychain(Keychain), + }, + } + + for _, option := range opts { + option(o) + } + + return o +} + +// WithJobs sets the number of concurrent jobs to run. +// +// The default number of jobs is GOMAXPROCS. +func WithJobs(jobs int) Option { + return func(o *options) { + o.jobs = jobs + } +} + +// WithTransport is a functional option for overriding the default transport +// for remote operations. +func WithTransport(t http.RoundTripper) Option { + return func(o *options) { + o.remote = append(o.remote, remote.WithTransport(t)) + o.google = append(o.google, google.WithTransport(t)) + o.crane = append(o.crane, crane.WithTransport(t)) + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. +func WithUserAgent(ua string) Option { + return func(o *options) { + o.remote = append(o.remote, remote.WithUserAgent(ua)) + o.google = append(o.google, google.WithUserAgent(ua)) + o.crane = append(o.crane, crane.WithUserAgent(ua)) + } +} + +// WithContext is a functional option for setting the context. +func WithContext(ctx context.Context) Option { + return func(o *options) { + o.remote = append(o.remote, remote.WithContext(ctx)) + o.google = append(o.google, google.WithContext(ctx)) + o.crane = append(o.crane, crane.WithContext(ctx)) + } +} + +// WithKeychain is a functional option for overriding the default +// authenticator for remote operations, using an authn.Keychain to find +// credentials. +// +// By default, gcrane will use gcrane.Keychain. +func WithKeychain(keys authn.Keychain) Option { + return func(o *options) { + // Replace the default keychain at position 0. + o.remote[0] = remote.WithAuthFromKeychain(keys) + o.google[0] = google.WithAuthFromKeychain(keys) + o.crane[0] = crane.WithAuthFromKeychain(keys) + } +} + +// WithAuth is a functional option for overriding the default authenticator +// for remote operations. +// +// By default, gcrane will use gcrane.Keychain. +func WithAuth(auth authn.Authenticator) Option { + return func(o *options) { + // Replace the default keychain at position 0. + o.remote[0] = remote.WithAuth(auth) + o.google[0] = google.WithAuth(auth) + o.crane[0] = crane.WithAuth(auth) + } +} diff --git a/pkg/gcrane/options_test.go b/pkg/gcrane/options_test.go new file mode 100644 index 0000000..c2ce2f9 --- /dev/null +++ b/pkg/gcrane/options_test.go @@ -0,0 +1,58 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gcrane + +import ( + "context" + "net/http" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" +) + +func TestOptions(t *testing.T) { + o := makeOptions() + if len(o.remote) != 1 { + t.Errorf("remote should default to Keychain") + } + if len(o.crane) != 1 { + t.Errorf("crane should default to Keychain") + } + if len(o.google) != 1 { + t.Errorf("google should default to Keychain") + } + + o = makeOptions(WithAuth(authn.Anonymous), WithKeychain(authn.DefaultKeychain)) + if len(o.remote) != 1 { + t.Errorf("WithKeychain should replace remote[0]") + } + if len(o.crane) != 1 { + t.Errorf("WithKeychain should replace crane[0]") + } + if len(o.google) != 1 { + t.Errorf("WithKeychain should replace google[0]") + } + + o = makeOptions(WithTransport(http.DefaultTransport), WithUserAgent("hi"), WithContext(context.TODO())) + if len(o.remote) != 4 { + t.Errorf("wrong number of options: %d", len(o.remote)) + } + if len(o.crane) != 4 { + t.Errorf("wrong number of options: %d", len(o.crane)) + } + if len(o.google) != 4 { + t.Errorf("wrong number of options: %d", len(o.google)) + } +} diff --git a/pkg/legacy/config.go b/pkg/legacy/config.go new file mode 100644 index 0000000..3364bec --- /dev/null +++ b/pkg/legacy/config.go @@ -0,0 +1,33 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package legacy + +import ( + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// LayerConfigFile is the configuration file that holds the metadata describing +// a v1 layer. See: +// https://github.com/moby/moby/blob/master/image/spec/v1.md +type LayerConfigFile struct { + v1.ConfigFile + + ContainerConfig v1.Config `json:"container_config,omitempty"` + + ID string `json:"id,omitempty"` + Parent string `json:"parent,omitempty"` + Throwaway bool `json:"throwaway,omitempty"` + Comment string `json:"comment,omitempty"` +} diff --git a/pkg/legacy/doc.go b/pkg/legacy/doc.go new file mode 100644 index 0000000..1d16688 --- /dev/null +++ b/pkg/legacy/doc.go @@ -0,0 +1,18 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package legacy provides functionality to work with docker images in the v1 +// format. +// See: https://github.com/moby/moby/blob/master/image/spec/v1.md +package legacy diff --git a/pkg/legacy/tarball/README.md b/pkg/legacy/tarball/README.md new file mode 100644 index 0000000..90b88c7 --- /dev/null +++ b/pkg/legacy/tarball/README.md @@ -0,0 +1,6 @@ +# `legacy/tarball` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/legacy/tarball) + +This package implements support for writing legacy tarballs, as described +[here](https://github.com/moby/moby/blob/749d90e10f989802638ae542daf54257f3bf71f2/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format). diff --git a/pkg/legacy/tarball/doc.go b/pkg/legacy/tarball/doc.go new file mode 100644 index 0000000..62684d6 --- /dev/null +++ b/pkg/legacy/tarball/doc.go @@ -0,0 +1,18 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tarball provides facilities for writing v1 docker images +// (https://github.com/moby/moby/blob/master/image/spec/v1.md) from/to a tarball +// on-disk. +package tarball diff --git a/pkg/legacy/tarball/write.go b/pkg/legacy/tarball/write.go new file mode 100644 index 0000000..e3f173c --- /dev/null +++ b/pkg/legacy/tarball/write.go @@ -0,0 +1,374 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "archive/tar" + "bytes" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/legacy" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// repositoriesTarDescriptor represents the repositories file inside a `docker save` tarball. +type repositoriesTarDescriptor map[string]map[string]string + +// v1Layer represents a layer with metadata needed by the v1 image spec https://github.com/moby/moby/blob/master/image/spec/v1.md. +type v1Layer struct { + // config is the layer metadata. + config *legacy.LayerConfigFile + // layer is the v1.Layer object this v1Layer represents. + layer v1.Layer +} + +// json returns the raw bytes of the json metadata of the given v1Layer. +func (l *v1Layer) json() ([]byte, error) { + return json.Marshal(l.config) +} + +// version returns the raw bytes of the "VERSION" file of the given v1Layer. +func (l *v1Layer) version() []byte { + return []byte("1.0") +} + +// v1LayerID computes the v1 image format layer id for the given v1.Layer with the given v1 parent ID and raw image config. +func v1LayerID(layer v1.Layer, parentID string, rawConfig []byte) (string, error) { + d, err := layer.Digest() + if err != nil { + return "", fmt.Errorf("unable to get layer digest to generate v1 layer ID: %w", err) + } + s := fmt.Sprintf("%s %s", d.Hex, parentID) + if len(rawConfig) != 0 { + s = fmt.Sprintf("%s %s", s, string(rawConfig)) + } + + h, _, _ := v1.SHA256(strings.NewReader(s)) + return h.Hex, nil +} + +// newTopV1Layer creates a new v1Layer for a layer other than the top layer in a v1 image tarball. +func newV1Layer(layer v1.Layer, parent *v1Layer, history v1.History) (*v1Layer, error) { + parentID := "" + if parent != nil { + parentID = parent.config.ID + } + id, err := v1LayerID(layer, parentID, nil) + if err != nil { + return nil, fmt.Errorf("unable to generate v1 layer ID: %w", err) + } + result := &v1Layer{ + layer: layer, + config: &legacy.LayerConfigFile{ + ConfigFile: v1.ConfigFile{ + Created: history.Created, + Author: history.Author, + }, + ContainerConfig: v1.Config{ + Cmd: []string{history.CreatedBy}, + }, + ID: id, + Parent: parentID, + Throwaway: history.EmptyLayer, + Comment: history.Comment, + }, + } + return result, nil +} + +// newTopV1Layer creates a new v1Layer for the top layer in a v1 image tarball. +func newTopV1Layer(layer v1.Layer, parent *v1Layer, history v1.History, imgConfig *v1.ConfigFile, rawConfig []byte) (*v1Layer, error) { + result, err := newV1Layer(layer, parent, history) + if err != nil { + return nil, err + } + id, err := v1LayerID(layer, result.config.Parent, rawConfig) + if err != nil { + return nil, fmt.Errorf("unable to generate v1 layer ID for top layer: %w", err) + } + result.config.ID = id + result.config.Architecture = imgConfig.Architecture + result.config.Container = imgConfig.Container + result.config.DockerVersion = imgConfig.DockerVersion + result.config.OS = imgConfig.OS + result.config.Config = imgConfig.Config + result.config.Created = imgConfig.Created + return result, nil +} + +// splitTag splits the given tagged image name <registry>/<repository>:<tag> +// into <registry>/<repository> and <tag>. +func splitTag(name string) (string, string) { + // Split on ":" + parts := strings.Split(name, ":") + // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation. + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { + base := strings.Join(parts[:len(parts)-1], ":") + tag := parts[len(parts)-1] + return base, tag + } + return name, "" +} + +// addTags adds the given image tags to the given "repositories" file descriptor in a v1 image tarball. +func addTags(repos repositoriesTarDescriptor, tags []string, topLayerID string) { + for _, t := range tags { + base, tag := splitTag(t) + tagToID, ok := repos[base] + if !ok { + tagToID = make(map[string]string) + repos[base] = tagToID + } + tagToID[tag] = topLayerID + } +} + +// updateLayerSources updates the given layer digest to descriptor map with the descriptor of the given layer in the given image if it's an undistributable layer. +func updateLayerSources(layerSources map[v1.Hash]v1.Descriptor, layer v1.Layer, img v1.Image) error { + d, err := layer.Digest() + if err != nil { + return err + } + // Add to LayerSources if it's a foreign layer. + desc, err := partial.BlobDescriptor(img, d) + if err != nil { + return err + } + if !desc.MediaType.IsDistributable() { + diffid, err := partial.BlobToDiffID(img, d) + if err != nil { + return err + } + layerSources[diffid] = *desc + } + return nil +} + +// Write is a wrapper to write a single image in V1 format and tag to a tarball. +func Write(ref name.Reference, img v1.Image, w io.Writer) error { + return MultiWrite(map[name.Reference]v1.Image{ref: img}, w) +} + +// filterEmpty filters out the history corresponding to empty layers from the +// given history. +func filterEmpty(h []v1.History) []v1.History { + result := []v1.History{} + for _, i := range h { + if i.EmptyLayer { + continue + } + result = append(result, i) + } + return result +} + +// MultiWrite writes the contents of each image to the provided reader, in the V1 image tarball format. +// The contents are written in the following format: +// One manifest.json file at the top level containing information about several images. +// One repositories file mapping from the image <registry>/<repo name> to <tag> to the id of the top most layer. +// For every layer, a directory named with the layer ID is created with the following contents: +// +// layer.tar - The uncompressed layer tarball. +// <layer id>.json- Layer metadata json. +// VERSION- Schema version string. Always set to "1.0". +// +// One file for the config blob, named after its SHA. +func MultiWrite(refToImage map[name.Reference]v1.Image, w io.Writer) error { + tf := tar.NewWriter(w) + defer tf.Close() + + sortedImages, imageToTags := dedupRefToImage(refToImage) + var m tarball.Manifest + repos := make(repositoriesTarDescriptor) + + seenLayerIDs := make(map[string]struct{}) + for _, img := range sortedImages { + tags := imageToTags[img] + + // Write the config. + cfgName, err := img.ConfigName() + if err != nil { + return err + } + cfgFileName := fmt.Sprintf("%s.json", cfgName.Hex) + cfgBlob, err := img.RawConfigFile() + if err != nil { + return err + } + if err := writeTarEntry(tf, cfgFileName, bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { + return err + } + cfg, err := img.ConfigFile() + if err != nil { + return err + } + + // Store foreign layer info. + layerSources := make(map[v1.Hash]v1.Descriptor) + + // Write the layers. + layers, err := img.Layers() + if err != nil { + return err + } + history := filterEmpty(cfg.History) + // Create a blank config history if the config didn't have a history. + if len(history) == 0 && len(layers) != 0 { + history = make([]v1.History, len(layers)) + } else if len(layers) != len(history) { + return fmt.Errorf("image config had layer history which did not match the number of layers, got len(history)=%d, len(layers)=%d, want len(history)=len(layers)", len(history), len(layers)) + } + layerFiles := make([]string, len(layers)) + var prev *v1Layer + for i, l := range layers { + if err := updateLayerSources(layerSources, l, img); err != nil { + return fmt.Errorf("unable to update image metadata to include undistributable layer source information: %w", err) + } + var cur *v1Layer + if i < (len(layers) - 1) { + cur, err = newV1Layer(l, prev, history[i]) + } else { + cur, err = newTopV1Layer(l, prev, history[i], cfg, cfgBlob) + } + if err != nil { + return err + } + layerFiles[i] = fmt.Sprintf("%s/layer.tar", cur.config.ID) + if _, ok := seenLayerIDs[cur.config.ID]; ok { + prev = cur + continue + } + seenLayerIDs[cur.config.ID] = struct{}{} + + // If the v1.Layer implements UncompressedSize efficiently, use that + // for the tar header. Otherwise, this iterates over Uncompressed(). + // NOTE: If using a streaming layer, this may consume the layer. + size, err := partial.UncompressedSize(l) + if err != nil { + return err + } + u, err := l.Uncompressed() + if err != nil { + return err + } + defer u.Close() + if err := writeTarEntry(tf, layerFiles[i], u, size); err != nil { + return err + } + + j, err := cur.json() + if err != nil { + return err + } + if err := writeTarEntry(tf, fmt.Sprintf("%s/json", cur.config.ID), bytes.NewReader(j), int64(len(j))); err != nil { + return err + } + v := cur.version() + if err := writeTarEntry(tf, fmt.Sprintf("%s/VERSION", cur.config.ID), bytes.NewReader(v), int64(len(v))); err != nil { + return err + } + prev = cur + } + + // Generate the tar descriptor and write it. + m = append(m, tarball.Descriptor{ + Config: cfgFileName, + RepoTags: tags, + Layers: layerFiles, + LayerSources: layerSources, + }) + // prev should be the top layer here. Use it to add the image tags + // to the tarball repositories file. + addTags(repos, tags, prev.config.ID) + } + + mBytes, err := json.Marshal(m) + if err != nil { + return err + } + + if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(mBytes), int64(len(mBytes))); err != nil { + return err + } + reposBytes, err := json.Marshal(&repos) + if err != nil { + return err + } + if err := writeTarEntry(tf, "repositories", bytes.NewReader(reposBytes), int64(len(reposBytes))); err != nil { + return err + } + return nil +} + +func dedupRefToImage(refToImage map[name.Reference]v1.Image) ([]v1.Image, map[v1.Image][]string) { + imageToTags := make(map[v1.Image][]string) + + for ref, img := range refToImage { + if tag, ok := ref.(name.Tag); ok { + if tags, ok := imageToTags[img]; ok && tags != nil { + imageToTags[img] = append(tags, tag.String()) + } else { + imageToTags[img] = []string{tag.String()} + } + } else { + if _, ok := imageToTags[img]; !ok { + imageToTags[img] = nil + } + } + } + + // Force specific order on tags + imgs := []v1.Image{} + for img, tags := range imageToTags { + sort.Strings(tags) + imgs = append(imgs, img) + } + + sort.Slice(imgs, func(i, j int) bool { + cfI, err := imgs[i].ConfigName() + if err != nil { + return false + } + cfJ, err := imgs[j].ConfigName() + if err != nil { + return false + } + return cfI.Hex < cfJ.Hex + }) + + return imgs, imageToTags +} + +// Writes a file to the provided writer with a corresponding tar header +func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error { + hdr := &tar.Header{ + Mode: 0644, + Typeflag: tar.TypeReg, + Size: size, + Name: path, + } + if err := tf.WriteHeader(hdr); err != nil { + return err + } + _, err := io.Copy(tf, r) + return err +} diff --git a/pkg/legacy/tarball/write_test.go b/pkg/legacy/tarball/write_test.go new file mode 100644 index 0000000..33e658c --- /dev/null +++ b/pkg/legacy/tarball/write_test.go @@ -0,0 +1,615 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestWrite(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + if err := Write(tag, randImage, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + + // Make sure the image is valid and can be loaded. + // Load it both by nil and by its name. + for _, it := range []*name.Tag{nil, &tag} { + tarImage, err := tarball.ImageFromPath(fp.Name(), it) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + if err := compare.Images(randImage, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } + + // Try loading a different tag, it should error. + fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error generating tag: %v", err) + } + if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil { + t.Errorf("Expected error loading tag %v from image", fakeTag) + } +} + +func TestMultiWriteSameImage(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image.") + } + + // Make two tags that point to the random image above. + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1.") + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2.") + } + dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test dig3.") + } + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage + refToImage[tag2] = randImage + refToImage[dig3] = randImage + + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + + // Write the images with both tags to the tarball + if err := MultiWrite(refToImage, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + if err := compare.Images(randImage, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } +} + +func TestMultiWriteDifferentImages(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage1, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 1: %v", err) + } + + // Make another random image + randImage2, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 2: %v", err) + } + + // Make another random image + randImage3, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 3: %v", err) + } + + // Create two tags, one pointing to each image created. + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1: %v", err) + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2: %v", err) + } + dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test dig3: %v", err) + } + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage1 + refToImage[tag2] = randImage2 + refToImage[dig3] = randImage3 + + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + + // Write both images to the tarball. + if err := MultiWrite(refToImage, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref, img := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + if err := compare.Images(img, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } +} + +func TestWriteForeignLayers(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 1) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + randLayer, err := random.Layer(512, types.DockerForeignLayer) + if err != nil { + t.Fatalf("random.Layer: %v", err) + } + img, err := mutate.Append(randImage, mutate.Addendum{ + Layer: randLayer, + URLs: []string{ + "example.com", + }, + }) + if err != nil { + t.Fatalf("Unable to mutate image to add foreign layer: %v", err) + } + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + if err := Write(tag, img, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Fatalf("validate.Image(): %v", err) + } + + m, err := tarImage.Manifest() + if err != nil { + t.Fatal(err) + } + + if got, want := m.Layers[1].MediaType, types.DockerForeignLayer; got != want { + t.Errorf("Wrong MediaType: %s != %s", got, want) + } + if got, want := m.Layers[1].URLs[0], "example.com"; got != want { + t.Errorf("Wrong URLs: %s != %s", got, want) + } +} + +func TestMultiWriteNoHistory(t *testing.T) { + // Make a random image. + img, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("Error getting image config: %v", err) + } + // Blank out the layer history. + cfg.History = nil + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + if err := Write(tag, img, fp); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Fatalf("validate.Image(): %v", err) + } +} + +func TestMultiWriteHistoryEmptyLayers(t *testing.T) { + // Build a history for 2 layers that is interspersed with empty layer + // history. + h := []v1.History{ + {EmptyLayer: true}, + {EmptyLayer: false}, + {EmptyLayer: true}, + {EmptyLayer: false}, + {EmptyLayer: true}, + } + // Make a random image with the number of non-empty layers from the history + // above. + img, err := random.Image(256, int64(len(filterEmpty(h)))) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("Error getting image config: %v", err) + } + // Override the config history with our custom built history that includes + // history for empty layers. + cfg.History = h + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + if err := Write(tag, img, fp); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Fatalf("validate.Image(): %v", err) + } +} + +func TestMultiWriteMismatchedHistory(t *testing.T) { + // Make a random image + img, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + cfg, err := img.ConfigFile() + if err != nil { + t.Fatalf("Error getting image config: %v", err) + } + + // Set the history such that number of history entries != layers. This + // should trigger an error during the image write. + cfg.History = make([]v1.History, 1) + img, err = mutate.ConfigFile(img, cfg) + if err != nil { + t.Fatalf("mutate.ConfigFile() = %v", err) + } + + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + err = Write(tag, img, fp) + if err == nil { + t.Fatal("Unexpected success writing tarball, got nil, want error.") + } + want := "image config had layer history which did not match the number of layers" + if !strings.Contains(err.Error(), want) { + t.Errorf("Got unexpected error when writing image with mismatched history & layer, got %v, want substring %q", err, want) + } +} + +type fastSizeLayer struct { + v1.Layer + size int64 + called bool +} + +func (l *fastSizeLayer) UncompressedSize() (int64, error) { + l.called = true + return l.size, nil +} + +func TestUncompressedSize(t *testing.T) { + // Make a random image + img, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + + rand, err := random.Layer(1000, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + + size, err := partial.UncompressedSize(rand) + if err != nil { + t.Fatal(err) + } + + l := &fastSizeLayer{Layer: rand, size: size} + + img, err = mutate.AppendLayers(img, l) + if err != nil { + t.Fatal(err) + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + if err := Write(tag, img, fp); err != nil { + t.Fatalf("Write(): %v", err) + } + if !l.called { + t.Errorf("expected UncompressedSize to be called, but it wasn't") + } +} + +// TestWriteSharedLayers tests that writing a tarball of multiple images that +// share some layers only writes those shared layers once. +func TestWriteSharedLayers(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + const baseImageLayerCount = 8 + + // Make a random image + baseImage, err := random.Image(256, baseImageLayerCount) + if err != nil { + t.Fatalf("Error creating base image: %v", err) + } + + // Make another random image + randLayer, err := random.Layer(256, types.DockerLayer) + if err != nil { + t.Fatalf("Error creating random layer %v", err) + } + extendedImage, err := mutate.Append(baseImage, mutate.Addendum{ + Layer: randLayer, + }) + if err != nil { + t.Fatalf("Error mutating base image %v", err) + } + + // Create two tags, one pointing to each image created. + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1: %v", err) + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2: %v", err) + } + refToImage := map[name.Reference]v1.Image{ + tag1: baseImage, + tag2: extendedImage, + } + + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + + // Write both images to the tarball. + if err := MultiWrite(refToImage, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref, img := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + if err := compare.Images(img, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } + + wantIDs := make(map[string]struct{}) + ids, err := v1LayerIDs(baseImage) + if err != nil { + t.Fatalf("Error getting base image IDs: %v", err) + } + for _, id := range ids { + wantIDs[id] = struct{}{} + } + ids, err = v1LayerIDs(extendedImage) + if err != nil { + t.Fatalf("Error getting extended image IDs: %v", err) + } + for _, id := range ids { + wantIDs[id] = struct{}{} + } + + // base + extended layer + different top base layer + if len(wantIDs) != baseImageLayerCount+2 { + t.Errorf("Expected to have %d unique layer IDs but have %d", baseImageLayerCount+2, len(wantIDs)) + } + + const layerFileName = "layer.tar" + r := tar.NewReader(fp) + for { + hdr, err := r.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + t.Fatalf("Get tar header: %v", err) + } + if filepath.Base(hdr.Name) == layerFileName { + id := filepath.Dir(hdr.Name) + if _, ok := wantIDs[id]; ok { + delete(wantIDs, id) + } else { + t.Errorf("Found unwanted layer with ID %q", id) + } + } + } + if len(wantIDs) != 0 { + for id := range wantIDs { + t.Errorf("Expected to find layer with ID %q but it didn't exist", id) + } + } +} + +func v1LayerIDs(img v1.Image) ([]string, error) { + layers, err := img.Layers() + if err != nil { + return nil, fmt.Errorf("get layers: %w", err) + } + ids := make([]string, len(layers)) + parentID := "" + for i, layer := range layers { + var rawCfg []byte + if i == len(layers)-1 { + rawCfg, err = img.RawConfigFile() + if err != nil { + return nil, fmt.Errorf("get raw config file: %w", err) + } + } + id, err := v1LayerID(layer, parentID, rawCfg) + if err != nil { + return nil, fmt.Errorf("get v1 layer ID: %w", err) + } + + ids[i] = id + parentID = id + } + return ids, nil +} diff --git a/pkg/logs/logs.go b/pkg/logs/logs.go new file mode 100644 index 0000000..a5d25b1 --- /dev/null +++ b/pkg/logs/logs.go @@ -0,0 +1,39 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package logs exposes the loggers used by this library. +package logs + +import ( + "io" + "log" +) + +var ( + // Warn is used to log non-fatal errors. + Warn = log.New(io.Discard, "", log.LstdFlags) + + // Progress is used to log notable, successful events. + Progress = log.New(io.Discard, "", log.LstdFlags) + + // Debug is used to log information that is useful for debugging. + Debug = log.New(io.Discard, "", log.LstdFlags) +) + +// Enabled checks to see if the logger's writer is set to something other +// than io.Discard. This allows callers to avoid expensive operations +// that will end up in /dev/null anyway. +func Enabled(l *log.Logger) bool { + return l.Writer() != io.Discard +} diff --git a/pkg/name/README.md b/pkg/name/README.md new file mode 100644 index 0000000..4889b84 --- /dev/null +++ b/pkg/name/README.md @@ -0,0 +1,3 @@ +# `name` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/name?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/name) diff --git a/pkg/name/check.go b/pkg/name/check.go new file mode 100644 index 0000000..e9a240a --- /dev/null +++ b/pkg/name/check.go @@ -0,0 +1,43 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "strings" + "unicode/utf8" +) + +// stripRunesFn returns a function which returns -1 (i.e. a value which +// signals deletion in strings.Map) for runes in 'runes', and the rune otherwise. +func stripRunesFn(runes string) func(rune) rune { + return func(r rune) rune { + if strings.ContainsRune(runes, r) { + return -1 + } + return r + } +} + +// checkElement checks a given named element matches character and length restrictions. +// Returns true if the given element adheres to the given restrictions, false otherwise. +func checkElement(name, element, allowedRunes string, minRunes, maxRunes int) error { + numRunes := utf8.RuneCountInString(element) + if (numRunes < minRunes) || (maxRunes < numRunes) { + return newErrBadName("%s must be between %d and %d characters in length: %s", name, minRunes, maxRunes, element) + } else if len(strings.Map(stripRunesFn(allowedRunes), element)) != 0 { + return newErrBadName("%s can only contain the characters `%s`: %s", name, allowedRunes, element) + } + return nil +} diff --git a/pkg/name/digest.go b/pkg/name/digest.go new file mode 100644 index 0000000..c049c1e --- /dev/null +++ b/pkg/name/digest.go @@ -0,0 +1,94 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + // nolint: depguard + _ "crypto/sha256" // Recommended by go-digest. + "strings" + + "github.com/opencontainers/go-digest" +) + +const digestDelim = "@" + +// Digest stores a digest name in a structured form. +type Digest struct { + Repository + digest string + original string +} + +// Ensure Digest implements Reference +var _ Reference = (*Digest)(nil) + +// Context implements Reference. +func (d Digest) Context() Repository { + return d.Repository +} + +// Identifier implements Reference. +func (d Digest) Identifier() string { + return d.DigestStr() +} + +// DigestStr returns the digest component of the Digest. +func (d Digest) DigestStr() string { + return d.digest +} + +// Name returns the name from which the Digest was derived. +func (d Digest) Name() string { + return d.Repository.Name() + digestDelim + d.DigestStr() +} + +// String returns the original input string. +func (d Digest) String() string { + return d.original +} + +// NewDigest returns a new Digest representing the given name. +func NewDigest(name string, opts ...Option) (Digest, error) { + // Split on "@" + parts := strings.Split(name, digestDelim) + if len(parts) != 2 { + return Digest{}, newErrBadName("a digest must contain exactly one '@' separator (e.g. registry/repository@digest) saw: %s", name) + } + base := parts[0] + dig := parts[1] + prefix := digest.Canonical.String() + ":" + if !strings.HasPrefix(dig, prefix) { + return Digest{}, newErrBadName("unsupported digest algorithm: %s", dig) + } + hex := strings.TrimPrefix(dig, prefix) + if err := digest.Canonical.Validate(hex); err != nil { + return Digest{}, err + } + + tag, err := NewTag(base, opts...) + if err == nil { + base = tag.Repository.Name() + } + + repo, err := NewRepository(base, opts...) + if err != nil { + return Digest{}, err + } + return Digest{ + Repository: repo, + digest: dig, + original: name, + }, nil +} diff --git a/pkg/name/digest_test.go b/pkg/name/digest_test.go new file mode 100644 index 0000000..85775cc --- /dev/null +++ b/pkg/name/digest_test.go @@ -0,0 +1,152 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "path" + "strings" + "testing" +) + +const validDigest = "sha256:deadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33fdeadb33f" + +var goodStrictValidationDigestNames = []string{ + "gcr.io/g-convoy/hello-world@" + validDigest, + "gcr.io/google.com/project-id/hello-world@" + validDigest, + "us.gcr.io/project-id/sub-repo@" + validDigest, + "example.text/foo/bar@" + validDigest, +} + +var goodStrictValidationTagDigestNames = []string{ + "example.text/foo/bar:latest@" + validDigest, + "example.text:8443/foo/bar:latest@" + validDigest, + "example.text/foo/bar:v1.0.0-alpine@" + validDigest, +} + +var goodWeakValidationDigestNames = []string{ + "namespace/pathcomponent/image@" + validDigest, + "library/ubuntu@" + validDigest, +} + +var goodWeakValidationTagDigestNames = []string{ + "nginx:latest@" + validDigest, + "library/nginx:latest@" + validDigest, +} + +var badDigestNames = []string{ + "gcr.io/project-id/unknown-alg@unknown:abc123", + "gcr.io/project-id/wrong-length@sha256:d34db33fd34db33f", + "gcr.io/project-id/missing-digest@", + // https://github.com/google/go-containerregistry/issues/1394 + "repo@sha256:" + strings.Repeat(":", 64), + "repo@sha256:" + strings.Repeat("sh", 32), + "repo@sha256:" + validDigest + "@" + validDigest, +} + +func TestNewDigestStrictValidation(t *testing.T) { + t.Parallel() + + for _, name := range goodStrictValidationDigestNames { + if digest, err := NewDigest(name, StrictValidation); err != nil { + t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err) + } else if digest.Name() != name { + t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", digest, name, digest.Name()) + } + } + + for _, name := range goodStrictValidationTagDigestNames { + if _, err := NewDigest(name, StrictValidation); err != nil { + t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err) + } + } + + for _, name := range append(goodWeakValidationDigestNames, badDigestNames...) { + if repo, err := NewDigest(name, StrictValidation); err == nil { + t.Errorf("`%s` should be an invalid Digest name, got Digest: %#v", name, repo) + } + } +} + +func TestNewDigest(t *testing.T) { + t.Parallel() + + for _, name := range append(goodStrictValidationDigestNames, append(goodWeakValidationDigestNames, goodWeakValidationTagDigestNames...)...) { + if _, err := NewDigest(name, WeakValidation); err != nil { + t.Errorf("`%s` should be a valid Digest name, got error: %v", name, err) + } + } + + for _, name := range badDigestNames { + if repo, err := NewDigest(name, WeakValidation); err == nil { + t.Errorf("`%s` should be an invalid Digest name, got Digest: %#v", name, repo) + } + } +} + +func TestDigestComponents(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepository := "project-id/image" + fullRepo := path.Join(testRegistry, testRepository) + + digestNameStr := testRegistry + "/" + testRepository + "@" + validDigest + digest, err := NewDigest(digestNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Digest name, got error: %v", digestNameStr, err) + } + + if got := digest.String(); got != digestNameStr { + t.Errorf("String() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, digestNameStr, got) + } + if got := digest.Identifier(); got != validDigest { + t.Errorf("Identifier() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, validDigest, got) + } + actualRegistry := digest.RegistryStr() + if actualRegistry != testRegistry { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, testRegistry, actualRegistry) + } + actualRepository := digest.RepositoryStr() + if actualRepository != testRepository { + t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, testRepository, actualRepository) + } + contextRepo := digest.Context().String() + if contextRepo != fullRepo { + t.Errorf("Context().String() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, fullRepo, contextRepo) + } + actualDigest := digest.DigestStr() + if actualDigest != validDigest { + t.Errorf("DigestStr() was incorrect for %v. Wanted: `%s` Got: `%s`", digest, validDigest, actualDigest) + } +} + +func TestDigestScopes(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepo := "project-id/image" + testAction := "pull" + + expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":") + + digestNameStr := testRegistry + "/" + testRepo + "@" + validDigest + digest, err := NewDigest(digestNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Digest name, got error: %v", digestNameStr, err) + } + + actualScope := digest.Scope(testAction) + if actualScope != expectedScope { + t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", digest, expectedScope, actualScope) + } +} diff --git a/pkg/name/doc.go b/pkg/name/doc.go new file mode 100644 index 0000000..b294794 --- /dev/null +++ b/pkg/name/doc.go @@ -0,0 +1,42 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package name defines structured types for representing image references. +// +// What's in a name? For image references, not nearly enough! +// +// Image references look a lot like URLs, but they differ in that they don't +// contain the scheme (http or https), they can end with a :tag or a @digest +// (the latter being validated), and they perform defaulting for missing +// components. +// +// Since image references don't contain the scheme, we do our best to infer +// if we use http or https from the given hostname. We allow http fallback for +// any host that looks like localhost (localhost, 127.0.0.1, ::1), ends in +// ".local", or is in the "private" address space per RFC 1918. For everything +// else, we assume https only. To override this heuristic, use the Insecure +// option. +// +// Image references with a digest signal to us that we should verify the content +// of the image matches the digest. E.g. when pulling a Digest reference, we'll +// calculate the sha256 of the manifest returned by the registry and error out +// if it doesn't match what we asked for. +// +// For defaulting, we interpret "ubuntu" as +// "index.docker.io/library/ubuntu:latest" because we add the missing repo +// "library", the missing registry "index.docker.io", and the missing tag +// "latest". To disable this defaulting, use the StrictValidation option. This +// is useful e.g. to only allow image references that explicitly set a tag or +// digest, so that you don't accidentally pull "latest". +package name diff --git a/pkg/name/errors.go b/pkg/name/errors.go new file mode 100644 index 0000000..bf004ff --- /dev/null +++ b/pkg/name/errors.go @@ -0,0 +1,48 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "errors" + "fmt" +) + +// ErrBadName is an error for when a bad docker name is supplied. +type ErrBadName struct { + info string +} + +func (e *ErrBadName) Error() string { + return e.info +} + +// Is reports whether target is an error of type ErrBadName +func (e *ErrBadName) Is(target error) bool { + var berr *ErrBadName + return errors.As(target, &berr) +} + +// newErrBadName returns a ErrBadName which returns the given formatted string from Error(). +func newErrBadName(fmtStr string, args ...any) *ErrBadName { + return &ErrBadName{fmt.Sprintf(fmtStr, args...)} +} + +// IsErrBadName returns true if the given error is an ErrBadName. +// +// Deprecated: Use errors.Is. +func IsErrBadName(err error) bool { + var berr *ErrBadName + return errors.As(err, &berr) +} diff --git a/pkg/name/errors_test.go b/pkg/name/errors_test.go new file mode 100644 index 0000000..a9ea4da --- /dev/null +++ b/pkg/name/errors_test.go @@ -0,0 +1,37 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "errors" + "testing" +) + +func TestBadName(t *testing.T) { + _, err := ParseReference("@@") + if !IsErrBadName(err) { + t.Errorf("Not an ErrBadName: %v", err) + } + var berr *ErrBadName + if !errors.As(err, &berr) { + t.Errorf("Not an ErrBadName using errors.As: %v", err) + } + if err.Error() != "could not parse reference: @@" { + t.Errorf("Unexpected string: %v", err) + } + if !errors.Is(err, &ErrBadName{}) { + t.Errorf("Not an ErrBadName using errors.Is: %v", err) + } +} diff --git a/pkg/name/internal/must_test.go b/pkg/name/internal/must_test.go new file mode 100644 index 0000000..d77d3ec --- /dev/null +++ b/pkg/name/internal/must_test.go @@ -0,0 +1,27 @@ +//go:build compile +// +build compile + +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "strings" + + "github.com/google/go-containerregistry/pkg/name" +) + +// This shouldn't compile. +var _ = name.MustParseReference(strings.Join([]string{"valid", "string"}, "/")) diff --git a/pkg/name/internal/must_test.sh b/pkg/name/internal/must_test.sh new file mode 100755 index 0000000..91a4fd1 --- /dev/null +++ b/pkg/name/internal/must_test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o nounset +set -o pipefail + +# Trying to compile without the build tag should work. +go test ./pkg/name/internal + +# Actually trying to compile should fail. +go test -tags=compile ./pkg/name/internal 2>&1 > /dev/null +if [[ $? -eq 0 ]]; then + echo "pkg/name/internal test compiled successfully, expected failure" + exit 1 +fi +echo "pkg/name/internal test successfully did not compile" diff --git a/pkg/name/options.go b/pkg/name/options.go new file mode 100644 index 0000000..d14fedc --- /dev/null +++ b/pkg/name/options.go @@ -0,0 +1,83 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +const ( + // DefaultRegistry is the registry name that will be used if no registry + // provided and the default is not overridden. + DefaultRegistry = "index.docker.io" + defaultRegistryAlias = "docker.io" + + // DefaultTag is the tag name that will be used if no tag provided and the + // default is not overridden. + DefaultTag = "latest" +) + +type options struct { + strict bool // weak by default + insecure bool // secure by default + defaultRegistry string + defaultTag string +} + +func makeOptions(opts ...Option) options { + opt := options{ + defaultRegistry: DefaultRegistry, + defaultTag: DefaultTag, + } + for _, o := range opts { + o(&opt) + } + return opt +} + +// Option is a functional option for name parsing. +type Option func(*options) + +// StrictValidation is an Option that requires image references to be fully +// specified; i.e. no defaulting for registry (dockerhub), repo (library), +// or tag (latest). +func StrictValidation(opts *options) { + opts.strict = true +} + +// WeakValidation is an Option that sets defaults when parsing names, see +// StrictValidation. +func WeakValidation(opts *options) { + opts.strict = false +} + +// Insecure is an Option that allows image references to be fetched without TLS. +func Insecure(opts *options) { + opts.insecure = true +} + +// OptionFn is a function that returns an option. +type OptionFn func() Option + +// WithDefaultRegistry sets the default registry that will be used if one is not +// provided. +func WithDefaultRegistry(r string) Option { + return func(opts *options) { + opts.defaultRegistry = r + } +} + +// WithDefaultTag sets the default tag that will be used if one is not provided. +func WithDefaultTag(t string) Option { + return func(opts *options) { + opts.defaultTag = t + } +} diff --git a/pkg/name/ref.go b/pkg/name/ref.go new file mode 100644 index 0000000..912ab33 --- /dev/null +++ b/pkg/name/ref.go @@ -0,0 +1,75 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "fmt" +) + +// Reference defines the interface that consumers use when they can +// take either a tag or a digest. +type Reference interface { + fmt.Stringer + + // Context accesses the Repository context of the reference. + Context() Repository + + // Identifier accesses the type-specific portion of the reference. + Identifier() string + + // Name is the fully-qualified reference name. + Name() string + + // Scope is the scope needed to access this reference. + Scope(string) string +} + +// ParseReference parses the string as a reference, either by tag or digest. +func ParseReference(s string, opts ...Option) (Reference, error) { + if t, err := NewTag(s, opts...); err == nil { + return t, nil + } + if d, err := NewDigest(s, opts...); err == nil { + return d, nil + } + return nil, newErrBadName("could not parse reference: " + s) +} + +type stringConst string + +// MustParseReference behaves like ParseReference, but panics instead of +// returning an error. It's intended for use in tests, or when a value is +// expected to be valid at code authoring time. +// +// To discourage its use in scenarios where the value is not known at code +// authoring time, it must be passed a string constant: +// +// const str = "valid/string" +// MustParseReference(str) +// MustParseReference("another/valid/string") +// MustParseReference(str + "/and/more") +// +// These will not compile: +// +// var str = "valid/string" +// MustParseReference(str) +// MustParseReference(strings.Join([]string{"valid", "string"}, "/")) +func MustParseReference(s stringConst, opts ...Option) Reference { + ref, err := ParseReference(string(s), opts...) + if err != nil { + panic(err) + } + return ref +} diff --git a/pkg/name/ref_test.go b/pkg/name/ref_test.go new file mode 100644 index 0000000..c47283c --- /dev/null +++ b/pkg/name/ref_test.go @@ -0,0 +1,157 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "testing" +) + +var ( + testDefaultRegistry = "registry.upbound.io" + testDefaultTag = "stable" + inputDefaultNames = []string{ + "crossplane/provider-gcp", + "crossplane/provider-gcp:v0.14.0", + "ubuntu", + "gcr.io/crossplane/provider-gcp:latest", + } + outputDefaultNames = []string{ + "registry.upbound.io/crossplane/provider-gcp:stable", + "registry.upbound.io/crossplane/provider-gcp:v0.14.0", + "registry.upbound.io/ubuntu:stable", + "gcr.io/crossplane/provider-gcp:latest", + } +) + +func TestParseReferenceDefaulting(t *testing.T) { + for i, name := range inputDefaultNames { + ref, err := ParseReference(name, WithDefaultRegistry(testDefaultRegistry), WithDefaultTag(testDefaultTag)) + if err != nil { + t.Errorf("ParseReference(%q); %v", name, err) + } + if ref.Name() != outputDefaultNames[i] { + t.Errorf("ParseReference(%q); got %v, want %v", name, ref.String(), outputDefaultNames[i]) + } + } +} + +func TestParseReference(t *testing.T) { + for _, name := range goodWeakValidationDigestNames { + ref, err := ParseReference(name, WeakValidation) + if err != nil { + t.Errorf("ParseReference(%q); %v", name, err) + } + dig, err := NewDigest(name, WeakValidation) + if err != nil { + t.Errorf("NewDigest(%q); %v", name, err) + } + if ref != dig { + t.Errorf("ParseReference(%q) != NewDigest(%q); got %v, want %v", name, name, ref, dig) + } + } + + for _, name := range goodStrictValidationDigestNames { + ref, err := ParseReference(name, StrictValidation) + if err != nil { + t.Errorf("ParseReference(%q); %v", name, err) + } + dig, err := NewDigest(name, StrictValidation) + if err != nil { + t.Errorf("NewDigest(%q); %v", name, err) + } + if ref != dig { + t.Errorf("ParseReference(%q) != NewDigest(%q); got %v, want %v", name, name, ref, dig) + } + } + + for _, name := range badDigestNames { + if _, err := ParseReference(name, WeakValidation); err == nil { + t.Errorf("ParseReference(%q); expected error, got none", name) + } + } + + for _, name := range goodWeakValidationTagNames { + ref, err := ParseReference(name, WeakValidation) + if err != nil { + t.Errorf("ParseReference(%q); %v", name, err) + } + tag, err := NewTag(name, WeakValidation) + if err != nil { + t.Errorf("NewTag(%q); %v", name, err) + } + if ref != tag { + t.Errorf("ParseReference(%q) != NewTag(%q); got %v, want %v", name, name, ref, tag) + } + } + + for _, name := range goodStrictValidationTagNames { + ref, err := ParseReference(name, StrictValidation) + if err != nil { + t.Errorf("ParseReference(%q); %v", name, err) + } + tag, err := NewTag(name, StrictValidation) + if err != nil { + t.Errorf("NewTag(%q); %v", name, err) + } + if ref != tag { + t.Errorf("ParseReference(%q) != NewTag(%q); got %v, want %v", name, name, ref, tag) + } + } + + for _, name := range badTagNames { + if _, err := ParseReference(name, WeakValidation); err == nil { + t.Errorf("ParseReference(%q); expected error, got none", name) + } + } +} + +func TestMustParseReference(t *testing.T) { + for _, name := range append(goodWeakValidationTagNames, goodWeakValidationDigestNames...) { + func() { + defer func() { + if err := recover(); err != nil { + t.Errorf("MustParseReference(%q, WeakValidation); panic: %v", name, err) + } + }() + MustParseReference(stringConst(name), WeakValidation) + }() + } + + for _, name := range append(goodStrictValidationTagNames, goodStrictValidationDigestNames...) { + func() { + defer func() { + if err := recover(); err != nil { + t.Errorf("MustParseReference(%q, StrictValidation); panic: %v", name, err) + } + }() + MustParseReference(stringConst(name), StrictValidation) + }() + } + + for _, name := range append(badTagNames, badDigestNames...) { + func() { + defer func() { recover() }() + ref := MustParseReference(stringConst(name), WeakValidation) + t.Errorf("MustParseReference(%q, WeakValidation) should panic, got: %#v", name, ref) + }() + } +} + +// Test that MustParseReference can accept a const string or string value. +const str = "valid/string" + +var _ = MustParseReference(str) +var _ = MustParseReference("valid/string") +var _ = MustParseReference("valid/prefix/" + str) diff --git a/pkg/name/registry.go b/pkg/name/registry.go new file mode 100644 index 0000000..2a26b66 --- /dev/null +++ b/pkg/name/registry.go @@ -0,0 +1,136 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "net" + "net/url" + "regexp" + "strings" +) + +// Detect more complex forms of local references. +var reLocal = regexp.MustCompile(`.*\.local(?:host)?(?::\d{1,5})?$`) + +// Detect the loopback IP (127.0.0.1) +var reLoopback = regexp.MustCompile(regexp.QuoteMeta("127.0.0.1")) + +// Detect the loopback IPV6 (::1) +var reipv6Loopback = regexp.MustCompile(regexp.QuoteMeta("::1")) + +// Registry stores a docker registry name in a structured form. +type Registry struct { + insecure bool + registry string +} + +// RegistryStr returns the registry component of the Registry. +func (r Registry) RegistryStr() string { + return r.registry +} + +// Name returns the name from which the Registry was derived. +func (r Registry) Name() string { + return r.RegistryStr() +} + +func (r Registry) String() string { + return r.Name() +} + +// Scope returns the scope required to access the registry. +func (r Registry) Scope(string) string { + // The only resource under 'registry' is 'catalog'. http://goo.gl/N9cN9Z + return "registry:catalog:*" +} + +func (r Registry) isRFC1918() bool { + ipStr := strings.Split(r.Name(), ":")[0] + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + for _, cidr := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} { + _, block, _ := net.ParseCIDR(cidr) + if block.Contains(ip) { + return true + } + } + return false +} + +// Scheme returns https scheme for all the endpoints except localhost or when explicitly defined. +func (r Registry) Scheme() string { + if r.insecure { + return "http" + } + if r.isRFC1918() { + return "http" + } + if strings.HasPrefix(r.Name(), "localhost:") { + return "http" + } + if reLocal.MatchString(r.Name()) { + return "http" + } + if reLoopback.MatchString(r.Name()) { + return "http" + } + if reipv6Loopback.MatchString(r.Name()) { + return "http" + } + return "https" +} + +func checkRegistry(name string) error { + // Per RFC 3986, registries (authorities) are required to be prefixed with "//" + // url.Host == hostname[:port] == authority + if url, err := url.Parse("//" + name); err != nil || url.Host != name { + return newErrBadName("registries must be valid RFC 3986 URI authorities: %s", name) + } + return nil +} + +// NewRegistry returns a Registry based on the given name. +// Strict validation requires explicit, valid RFC 3986 URI authorities to be given. +func NewRegistry(name string, opts ...Option) (Registry, error) { + opt := makeOptions(opts...) + if opt.strict && len(name) == 0 { + return Registry{}, newErrBadName("strict validation requires the registry to be explicitly defined") + } + + if err := checkRegistry(name); err != nil { + return Registry{}, err + } + + if name == "" { + name = opt.defaultRegistry + } + // Rewrite "docker.io" to "index.docker.io". + // See: https://github.com/google/go-containerregistry/issues/68 + if name == defaultRegistryAlias { + name = DefaultRegistry + } + + return Registry{registry: name, insecure: opt.insecure}, nil +} + +// NewInsecureRegistry returns an Insecure Registry based on the given name. +// +// Deprecated: Use the Insecure Option with NewRegistry instead. +func NewInsecureRegistry(name string, opts ...Option) (Registry, error) { + opts = append(opts, Insecure) + return NewRegistry(name, opts...) +} diff --git a/pkg/name/registry_test.go b/pkg/name/registry_test.go new file mode 100644 index 0000000..9986ee6 --- /dev/null +++ b/pkg/name/registry_test.go @@ -0,0 +1,252 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "testing" +) + +var goodStrictValidationRegistryNames = []string{ + "gcr.io", + "gcr.io:9001", + "index.docker.io", + "us.gcr.io", + "example.text", + "localhost", + "localhost:9090", +} + +var goodWeakValidationRegistryNames = []string{ + "", +} + +var badRegistryNames = []string{ + "white space", + "gcr?com", +} + +func TestNewRegistryStrictValidation(t *testing.T) { + t.Parallel() + + for _, name := range goodStrictValidationRegistryNames { + if registry, err := NewRegistry(name, StrictValidation); err != nil { + t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err) + } else { + if registry.Name() != name { + t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", registry, name, registry.Name()) + } + if registry.String() != name { + t.Errorf("`%v` .String() should reproduce the original name. Wanted: %s Got: %s", registry, name, registry.String()) + } + } + } + + for _, name := range append(goodWeakValidationRegistryNames, badRegistryNames...) { + if repo, err := NewRegistry(name, StrictValidation); err == nil { + t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo) + } + } +} + +func TestNewRegistry(t *testing.T) { + t.Parallel() + + for _, name := range append(goodStrictValidationRegistryNames, goodWeakValidationRegistryNames...) { + if _, err := NewRegistry(name, WeakValidation); err != nil { + t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err) + } + } + + for _, name := range badRegistryNames { + if repo, err := NewRegistry(name, WeakValidation); err == nil { + t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo) + } + } +} + +func TestNewInsecureRegistry(t *testing.T) { + t.Parallel() + + for _, name := range append(goodStrictValidationRegistryNames, goodWeakValidationRegistryNames...) { + if _, err := NewInsecureRegistry(name, WeakValidation); err != nil { + t.Errorf("`%s` should be a valid Registry name, got error: %v", name, err) + } + } + + for _, name := range badRegistryNames { + if repo, err := NewInsecureRegistry(name, WeakValidation); err == nil { + t.Errorf("`%s` should be an invalid Registry name, got Registry: %#v", name, repo) + } + } +} + +func TestDefaultRegistryNames(t *testing.T) { + testRegistries := []string{"docker.io", ""} + + for _, testRegistry := range testRegistries { + registry, err := NewRegistry(testRegistry, WeakValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err) + } + + actualRegistry := registry.RegistryStr() + if actualRegistry != DefaultRegistry { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, DefaultRegistry, actualRegistry) + } + } +} + +func TestOverrideDefaultRegistryNames(t *testing.T) { + testRegistries := []string{"docker.io", ""} + expectedRegistries := []string{"index.docker.io", "gcr.io"} + overrideDefault := "gcr.io" + + for i, testRegistry := range testRegistries { + registry, err := NewRegistry(testRegistry, WeakValidation, WithDefaultRegistry(overrideDefault)) + if err != nil { + t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err) + } + + actualRegistry := registry.RegistryStr() + if actualRegistry != expectedRegistries[i] { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, expectedRegistries[i], actualRegistry) + } + } +} + +func TestRegistryComponents(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + + registry, err := NewRegistry(testRegistry, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err) + } + + actualRegistry := registry.RegistryStr() + if actualRegistry != testRegistry { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", registry, testRegistry, actualRegistry) + } +} + +func TestRegistryScopes(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testAction := "whatever" + + expectedScope := "registry:catalog:*" + + registry, err := NewRegistry(testRegistry, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Registry name, got error: %v", testRegistry, err) + } + + actualScope := registry.Scope(testAction) + if actualScope != expectedScope { + t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", registry, expectedScope, actualScope) + } +} + +func TestIsRFC1918(t *testing.T) { + t.Parallel() + tests := []struct { + reg string + result bool + }{{ + reg: "index.docker.io", + result: false, + }, { + reg: "10.2.3.4:5000", + result: true, + }, { + reg: "8.8.8.8", + result: false, + }, { + reg: "172.16.3.4:3000", + result: true, + }, { + reg: "192.168.3.4", + result: true, + }, { + reg: "10.256.0.0:5000", + result: false, + }} + for _, test := range tests { + reg, err := NewRegistry(test.reg, WeakValidation) + if err != nil { + t.Errorf("NewRegistry(%s) = %v", test.reg, err) + } + got := reg.isRFC1918() + if got != test.result { + t.Errorf("isRFC1918(); got %v, want %v", got, test.result) + } + } +} + +func TestRegistryScheme(t *testing.T) { + t.Parallel() + tests := []struct { + domain string + scheme string + }{{ + domain: "foo.svc.local:1234", + scheme: "http", + }, { + domain: "127.0.0.1:1234", + scheme: "http", + }, { + domain: "127.0.0.1", + scheme: "http", + }, { + domain: "localhost:8080", + scheme: "http", + }, { + domain: "gcr.io", + scheme: "https", + }, { + domain: "index.docker.io", + scheme: "https", + }, { + domain: "::1", + scheme: "http", + }, { + domain: "10.2.3.4:5000", + scheme: "http", + }} + + for _, test := range tests { + reg, err := NewRegistry(test.domain, WeakValidation) + if err != nil { + t.Errorf("NewRegistry(%s) = %v", test.domain, err) + } + if got, want := reg.Scheme(), test.scheme; got != want { + t.Errorf("scheme(%v); got %v, want %v", reg, got, want) + } + } +} + +func TestRegistryInsecureScheme(t *testing.T) { + t.Parallel() + domain := "gcr.io" + + reg, err := NewInsecureRegistry(domain, WeakValidation) + if err != nil { + t.Errorf("NewRegistry(%s) = %v", domain, err) + } + + if got := reg.Scheme(); got != "http" { + t.Errorf("scheme(%v); got %v, want http", reg, got) + } +} diff --git a/pkg/name/repository.go b/pkg/name/repository.go new file mode 100644 index 0000000..9250e36 --- /dev/null +++ b/pkg/name/repository.go @@ -0,0 +1,121 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "fmt" + "strings" +) + +const ( + defaultNamespace = "library" + repositoryChars = "abcdefghijklmnopqrstuvwxyz0123456789_-./" + regRepoDelimiter = "/" +) + +// Repository stores a docker repository name in a structured form. +type Repository struct { + Registry + repository string +} + +// See https://docs.docker.com/docker-hub/official_repos +func hasImplicitNamespace(repo string, reg Registry) bool { + return !strings.ContainsRune(repo, '/') && reg.RegistryStr() == DefaultRegistry +} + +// RepositoryStr returns the repository component of the Repository. +func (r Repository) RepositoryStr() string { + if hasImplicitNamespace(r.repository, r.Registry) { + return fmt.Sprintf("%s/%s", defaultNamespace, r.repository) + } + return r.repository +} + +// Name returns the name from which the Repository was derived. +func (r Repository) Name() string { + regName := r.Registry.Name() + if regName != "" { + return regName + regRepoDelimiter + r.RepositoryStr() + } + // TODO: As far as I can tell, this is unreachable. + return r.RepositoryStr() +} + +func (r Repository) String() string { + return r.Name() +} + +// Scope returns the scope required to perform the given action on the registry. +// TODO(jonjohnsonjr): consider moving scopes to a separate package. +func (r Repository) Scope(action string) string { + return fmt.Sprintf("repository:%s:%s", r.RepositoryStr(), action) +} + +func checkRepository(repository string) error { + return checkElement("repository", repository, repositoryChars, 2, 255) +} + +// NewRepository returns a new Repository representing the given name, according to the given strictness. +func NewRepository(name string, opts ...Option) (Repository, error) { + opt := makeOptions(opts...) + if len(name) == 0 { + return Repository{}, newErrBadName("a repository name must be specified") + } + + var registry string + repo := name + parts := strings.SplitN(name, regRepoDelimiter, 2) + if len(parts) == 2 && (strings.ContainsRune(parts[0], '.') || strings.ContainsRune(parts[0], ':')) { + // The first part of the repository is treated as the registry domain + // iff it contains a '.' or ':' character, otherwise it is all repository + // and the domain defaults to Docker Hub. + registry = parts[0] + repo = parts[1] + } + + if err := checkRepository(repo); err != nil { + return Repository{}, err + } + + reg, err := NewRegistry(registry, opts...) + if err != nil { + return Repository{}, err + } + if hasImplicitNamespace(repo, reg) && opt.strict { + return Repository{}, newErrBadName("strict validation requires the full repository path (missing 'library')") + } + return Repository{reg, repo}, nil +} + +// Tag returns a Tag in this Repository. +func (r Repository) Tag(identifier string) Tag { + t := Tag{ + tag: identifier, + Repository: r, + } + t.original = t.Name() + return t +} + +// Digest returns a Digest in this Repository. +func (r Repository) Digest(identifier string) Digest { + d := Digest{ + digest: identifier, + Repository: r, + } + d.original = d.Name() + return d +} diff --git a/pkg/name/repository_test.go b/pkg/name/repository_test.go new file mode 100644 index 0000000..790cab6 --- /dev/null +++ b/pkg/name/repository_test.go @@ -0,0 +1,145 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "errors" + "strings" + "testing" +) + +var goodStrictValidationRepositoryNames = []string{ + "gcr.io/g-convoy/hello-world", + "gcr.io/google.com/project-id/hello-world", + "us.gcr.io/project-id/sub-repo", + "example.text/foo/bar", + "mirror.gcr.io/ubuntu", + "index.docker.io/library/ubuntu", +} + +var goodWeakValidationRepositoryNames = []string{ + "namespace/pathcomponent/image", + "library/ubuntu", + "ubuntu", +} + +var badRepositoryNames = []string{ + "white space", + "b@char/image", + "", +} + +func TestNewRepositoryStrictValidation(t *testing.T) { + t.Parallel() + + for _, name := range goodStrictValidationRepositoryNames { + if repository, err := NewRepository(name, StrictValidation); err != nil { + t.Errorf("`%s` should be a valid Repository name, got error: %v", name, err) + } else if repository.Name() != name { + t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", repository, name, repository.Name()) + } + } + + for _, name := range append(goodWeakValidationRepositoryNames, badRepositoryNames...) { + if repo, err := NewRepository(name, StrictValidation); err == nil { + t.Errorf("`%s` should be an invalid repository name, got Repository: %#v", name, repo) + } + } +} + +func TestNewRepository(t *testing.T) { + t.Parallel() + + for _, name := range append(goodStrictValidationRepositoryNames, goodWeakValidationRepositoryNames...) { + if _, err := NewRepository(name, WeakValidation); err != nil { + t.Errorf("`%s` should be a valid repository name, got error: %v", name, err) + } + } + + for _, name := range badRepositoryNames { + if repo, err := NewRepository(name, WeakValidation); err == nil { + t.Errorf("`%s` should be an invalid repository name, got Repository: %#v", name, repo) + } + } +} + +func TestRepositoryComponents(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepository := "project-id/image" + + repositoryNameStr := testRegistry + "/" + testRepository + repository, err := NewRepository(repositoryNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Repository name, got error: %v", repositoryNameStr, err) + } + + actualRegistry := repository.RegistryStr() + if actualRegistry != testRegistry { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", repository, testRegistry, actualRegistry) + } + actualRepository := repository.RepositoryStr() + if actualRepository != testRepository { + t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", repository, testRepository, actualRepository) + } +} + +func TestRepositoryScopes(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepo := "project-id/image" + testAction := "pull" + + expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":") + + repositoryNameStr := testRegistry + "/" + testRepo + repository, err := NewRepository(repositoryNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Repository name, got error: %v", repositoryNameStr, err) + } + + actualScope := repository.Scope(testAction) + if actualScope != expectedScope { + t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", repository, expectedScope, actualScope) + } +} + +func TestRepositoryBadDefaulting(t *testing.T) { + var berr *ErrBadName + if _, err := NewRepository("index.docker.io/foo", StrictValidation); !errors.As(err, &berr) { + t.Errorf("Not an ErrBadName: %v", err) + } +} + +func TestRepositoryChildren(t *testing.T) { + repo, err := NewRepository("example.com/repo", Insecure) + if err != nil { + t.Fatal(err) + } + tag := repo.Tag("foo") + if got, want := tag.Scheme(), "http"; got != want { + t.Errorf("tag.Scheme(): got %s want %s", got, want) + } + if got, want := tag.String(), "example.com/repo:foo"; got != want { + t.Errorf("tag.String(): got %s want %s", got, want) + } + digest := repo.Digest("badf00d") + if got, want := digest.Scheme(), "http"; got != want { + t.Errorf("digest.Scheme(): got %s want %s", got, want) + } + if got, want := digest.String(), "example.com/repo@badf00d"; got != want { + t.Errorf("digest.String(): got %s want %s", got, want) + } +} diff --git a/pkg/name/tag.go b/pkg/name/tag.go new file mode 100644 index 0000000..66bd1be --- /dev/null +++ b/pkg/name/tag.go @@ -0,0 +1,108 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "strings" +) + +const ( + // TODO(dekkagaijin): use the docker/distribution regexes for validation. + tagChars = "abcdefghijklmnopqrstuvwxyz0123456789_-.ABCDEFGHIJKLMNOPQRSTUVWXYZ" + tagDelim = ":" +) + +// Tag stores a docker tag name in a structured form. +type Tag struct { + Repository + tag string + original string +} + +// Ensure Tag implements Reference +var _ Reference = (*Tag)(nil) + +// Context implements Reference. +func (t Tag) Context() Repository { + return t.Repository +} + +// Identifier implements Reference. +func (t Tag) Identifier() string { + return t.TagStr() +} + +// TagStr returns the tag component of the Tag. +func (t Tag) TagStr() string { + return t.tag +} + +// Name returns the name from which the Tag was derived. +func (t Tag) Name() string { + return t.Repository.Name() + tagDelim + t.TagStr() +} + +// String returns the original input string. +func (t Tag) String() string { + return t.original +} + +// Scope returns the scope required to perform the given action on the tag. +func (t Tag) Scope(action string) string { + return t.Repository.Scope(action) +} + +func checkTag(name string) error { + return checkElement("tag", name, tagChars, 1, 128) +} + +// NewTag returns a new Tag representing the given name, according to the given strictness. +func NewTag(name string, opts ...Option) (Tag, error) { + opt := makeOptions(opts...) + base := name + tag := "" + + // Split on ":" + parts := strings.Split(name, tagDelim) + // Verify that we aren't confusing a tag for a hostname w/ port for the purposes of weak validation. + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], regRepoDelimiter) { + base = strings.Join(parts[:len(parts)-1], tagDelim) + tag = parts[len(parts)-1] + } + + // We don't require a tag, but if we get one check it's valid, + // even when not being strict. + // If we are being strict, we want to validate the tag regardless in case + // it's empty. + if tag != "" || opt.strict { + if err := checkTag(tag); err != nil { + return Tag{}, err + } + } + + if tag == "" { + tag = opt.defaultTag + } + + repo, err := NewRepository(base, opts...) + if err != nil { + return Tag{}, err + } + return Tag{ + Repository: repo, + tag: tag, + original: name, + }, nil +} diff --git a/pkg/name/tag_test.go b/pkg/name/tag_test.go new file mode 100644 index 0000000..e340566 --- /dev/null +++ b/pkg/name/tag_test.go @@ -0,0 +1,162 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package name + +import ( + "path" + "strings" + "testing" +) + +var goodStrictValidationTagNames = []string{ + "gcr.io/g-convoy/hello-world:latest", + "gcr.io/google.com/g-convoy/hello-world:latest", + "gcr.io/project-id/with-nums:v2", + "us.gcr.io/project-id/image:with.period.in.tag", + "gcr.io/project-id/image:w1th-alpha_num3ric.PLUScaps", + "domain.with.port:9001/image:latest", +} + +var goodWeakValidationTagNames = []string{ + "namespace/pathcomponent/image", + "library/ubuntu", + "gcr.io/project-id/implicit-latest", + "www.example.test:12345/repo/path", +} + +var badTagNames = []string{ + "gcr.io/project-id/bad_chars:c@n'tuse", + "gcr.io/project-id/wrong-length:white space", + "gcr.io/project-id/too-many-chars:thisisthetagthatneverendsitgoesonandonmyfriendsomepeoplestartedtaggingitnotknowingwhatitwasandtheyllcontinuetaggingitforeverjustbecausethisisthetagthatneverends", +} + +func TestNewTagStrictValidation(t *testing.T) { + t.Parallel() + + for _, name := range goodStrictValidationTagNames { + if tag, err := NewTag(name, StrictValidation); err != nil { + t.Errorf("`%s` should be a valid Tag name, got error: %v", name, err) + } else if tag.Name() != name { + t.Errorf("`%v` .Name() should reproduce the original name. Wanted: %s Got: %s", tag, name, tag.Name()) + } + } + + for _, name := range append(goodWeakValidationTagNames, badTagNames...) { + if tag, err := NewTag(name, StrictValidation); err == nil { + t.Errorf("`%s` should be an invalid Tag name, got Tag: %#v", name, tag) + } + } +} + +func TestNewTag(t *testing.T) { + t.Parallel() + + for _, name := range append(goodStrictValidationTagNames, goodWeakValidationTagNames...) { + if _, err := NewTag(name, WeakValidation); err != nil { + t.Errorf("`%s` should be a valid Tag name, got error: %v", name, err) + } + } + + for _, name := range badTagNames { + if tag, err := NewTag(name, WeakValidation); err == nil { + t.Errorf("`%s` should be an invalid Tag name, got Tag: %#v", name, tag) + } + } +} + +func TestTagComponents(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepository := "project-id/image" + testTag := "latest" + fullRepo := path.Join(testRegistry, testRepository) + + tagNameStr := testRegistry + "/" + testRepository + ":" + testTag + tag, err := NewTag(tagNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err) + } + + actualRegistry := tag.RegistryStr() + if actualRegistry != testRegistry { + t.Errorf("RegistryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testRegistry, actualRegistry) + } + actualRepository := tag.RepositoryStr() + if actualRepository != testRepository { + t.Errorf("RepositoryStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testRepository, actualRepository) + } + actualTag := tag.TagStr() + if actualTag != testTag { + t.Errorf("TagStr() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, testTag, actualTag) + } + if got, want := tag.Context().String(), fullRepo; got != want { + t.Errorf("Context.String() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got) + } + if got, want := tag.Identifier(), testTag; got != want { + t.Errorf("Identifier() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got) + } + if got, want := tag.String(), tagNameStr; got != want { + t.Errorf("String() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, want, got) + } +} + +func TestTagScopes(t *testing.T) { + t.Parallel() + testRegistry := "gcr.io" + testRepo := "project-id/image" + testTag := "latest" + testAction := "pull" + + expectedScope := strings.Join([]string{"repository", testRepo, testAction}, ":") + + tagNameStr := testRegistry + "/" + testRepo + ":" + testTag + tag, err := NewTag(tagNameStr, StrictValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err) + } + + actualScope := tag.Scope(testAction) + if actualScope != expectedScope { + t.Errorf("scope was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedScope, actualScope) + } +} + +func TestAllDefaults(t *testing.T) { + tagNameStr := "ubuntu" + tag, err := NewTag(tagNameStr, WeakValidation) + if err != nil { + t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err) + } + + expectedName := "index.docker.io/library/ubuntu:latest" + actualName := tag.Name() + if actualName != expectedName { + t.Errorf("Name() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedName, actualName) + } +} + +func TestOverrideDefault(t *testing.T) { + tagNameStr := "ubuntu" + tag, err := NewTag(tagNameStr, WeakValidation, WithDefaultTag("other")) + if err != nil { + t.Fatalf("`%s` should be a valid Tag name, got error: %v", tagNameStr, err) + } + + expectedName := "index.docker.io/library/ubuntu:other" + actualName := tag.Name() + if actualName != expectedName { + t.Errorf("Name() was incorrect for %v. Wanted: `%s` Got: `%s`", tag, expectedName, actualName) + } +} diff --git a/pkg/registry/README.md b/pkg/registry/README.md new file mode 100644 index 0000000..5e58bbc --- /dev/null +++ b/pkg/registry/README.md @@ -0,0 +1,14 @@ +# `pkg/registry` + +This package implements a Docker v2 registry and the OCI distribution specification. + +It is designed to be used anywhere a low dependency container registry is needed, with an initial focus on tests. + +Its goal is to be standards compliant and its strictness will increase over time. + +This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it in production, please let us know how and send us PRs for integration tests. + +Before sending a PR, understand that the expectation of this package is that it remain free of extraneous dependencies. +This means that we expect `pkg/registry` to only have dependencies on Go's standard library, and other packages in `go-containerregistry`. + +You may be asked to change your code to reduce dependencies, and your PR might be rejected if this is deemed impossible. diff --git a/pkg/registry/blobs.go b/pkg/registry/blobs.go new file mode 100644 index 0000000..4bf2c65 --- /dev/null +++ b/pkg/registry/blobs.go @@ -0,0 +1,483 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "path" + "strings" + "sync" + + "github.com/google/go-containerregistry/internal/verify" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// Returns whether this url should be handled by the blob handler +// This is complicated because blob is indicated by the trailing path, not the leading path. +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-a-layer +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-a-layer +func isBlob(req *http.Request) bool { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + if len(elem) < 3 { + return false + } + return elem[len(elem)-2] == "blobs" || (elem[len(elem)-3] == "blobs" && + elem[len(elem)-2] == "uploads") +} + +// blobHandler represents a minimal blob storage backend, capable of serving +// blob contents. +type blobHandler interface { + // Get gets the blob contents, or errNotFound if the blob wasn't found. + Get(ctx context.Context, repo string, h v1.Hash) (io.ReadCloser, error) +} + +// blobStatHandler is an extension interface representing a blob storage +// backend that can serve metadata about blobs. +type blobStatHandler interface { + // Stat returns the size of the blob, or errNotFound if the blob wasn't + // found, or redirectError if the blob can be found elsewhere. + Stat(ctx context.Context, repo string, h v1.Hash) (int64, error) +} + +// blobPutHandler is an extension interface representing a blob storage backend +// that can write blob contents. +type blobPutHandler interface { + // Put puts the blob contents. + // + // The contents will be verified against the expected size and digest + // as the contents are read, and an error will be returned if these + // don't match. Implementations should return that error, or a wrapper + // around that error, to return the correct error when these don't match. + Put(ctx context.Context, repo string, h v1.Hash, rc io.ReadCloser) error +} + +// blobDeleteHandler is an extension interface representing a blob storage +// backend that can delete blob contents. +type blobDeleteHandler interface { + // Delete the blob contents. + Delete(ctx context.Context, repo string, h v1.Hash) error +} + +// redirectError represents a signal that the blob handler doesn't have the blob +// contents, but that those contents are at another location which registry +// clients should redirect to. +type redirectError struct { + // Location is the location to find the contents. + Location string + + // Code is the HTTP redirect status code to return to clients. + Code int +} + +func (e redirectError) Error() string { return fmt.Sprintf("redirecting (%d): %s", e.Code, e.Location) } + +// errNotFound represents an error locating the blob. +var errNotFound = errors.New("not found") + +type memHandler struct { + m map[string][]byte + lock sync.Mutex +} + +func (m *memHandler) Stat(_ context.Context, _ string, h v1.Hash) (int64, error) { + m.lock.Lock() + defer m.lock.Unlock() + + b, found := m.m[h.String()] + if !found { + return 0, errNotFound + } + return int64(len(b)), nil +} +func (m *memHandler) Get(_ context.Context, _ string, h v1.Hash) (io.ReadCloser, error) { + m.lock.Lock() + defer m.lock.Unlock() + + b, found := m.m[h.String()] + if !found { + return nil, errNotFound + } + return io.NopCloser(bytes.NewReader(b)), nil +} +func (m *memHandler) Put(_ context.Context, _ string, h v1.Hash, rc io.ReadCloser) error { + m.lock.Lock() + defer m.lock.Unlock() + + defer rc.Close() + all, err := io.ReadAll(rc) + if err != nil { + return err + } + m.m[h.String()] = all + return nil +} +func (m *memHandler) Delete(_ context.Context, _ string, h v1.Hash) error { + m.lock.Lock() + defer m.lock.Unlock() + + if _, found := m.m[h.String()]; !found { + return errNotFound + } + + delete(m.m, h.String()) + return nil +} + +// blobs +type blobs struct { + blobHandler blobHandler + + // Each upload gets a unique id that writes occur to until finalized. + uploads map[string][]byte + lock sync.Mutex + log *log.Logger +} + +func (b *blobs) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + if elem[len(elem)-1] == "" { + elem = elem[:len(elem)-1] + } + // Must have a path of form /v2/{name}/blobs/{upload,sha256:} + if len(elem) < 4 { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "blobs must be attached to a repo", + } + } + target := elem[len(elem)-1] + service := elem[len(elem)-2] + digest := req.URL.Query().Get("digest") + contentRange := req.Header.Get("Content-Range") + + repo := req.URL.Host + path.Join(elem[1:len(elem)-2]...) + + switch req.Method { + case http.MethodHead: + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + var size int64 + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + } else { + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + defer rc.Close() + size, err = io.Copy(io.Discard, rc) + if err != nil { + return regErrInternal(err) + } + } + + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusOK) + return nil + + case http.MethodGet: + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + var size int64 + var r io.Reader + if bsh, ok := b.blobHandler.(blobStatHandler); ok { + size, err = bsh.Stat(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + return regErrInternal(err) + } + + rc, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return regErrInternal(err) + } + defer rc.Close() + r = rc + } else { + tmp, err := b.blobHandler.Get(req.Context(), repo, h) + if errors.Is(err, errNotFound) { + return regErrBlobUnknown + } else if err != nil { + var rerr redirectError + if errors.As(err, &rerr) { + http.Redirect(resp, req, rerr.Location, rerr.Code) + return nil + } + + return regErrInternal(err) + } + defer tmp.Close() + var buf bytes.Buffer + io.Copy(&buf, tmp) + size = int64(buf.Len()) + r = &buf + } + + resp.Header().Set("Content-Length", fmt.Sprint(size)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, r) + return nil + + case http.MethodPost: + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { + return regErrUnsupported + } + + // It is weird that this is "target" instead of "service", but + // that's how the index math works out above. + if target != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("POST to /blobs must be followed by /uploads, got %s", target), + } + } + + if digest != "" { + h, err := v1.NewHash(digest) + if err != nil { + return regErrDigestInvalid + } + + vrc, err := verify.ReadCloser(req.Body, req.ContentLength, h) + if err != nil { + return regErrInternal(err) + } + defer vrc.Close() + + if err = bph.Put(req.Context(), repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) + return regErrDigestMismatch + } + return regErrInternal(err) + } + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusCreated) + return nil + } + + id := fmt.Sprint(rand.Int63()) + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-2]...), "blobs/uploads", id)) + resp.Header().Set("Range", "0-0") + resp.WriteHeader(http.StatusAccepted) + return nil + + case http.MethodPatch: + if service != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("PATCH to /blobs must be followed by /uploads, got %s", service), + } + } + + if contentRange != "" { + start, end := 0, 0 + if _, err := fmt.Sscanf(contentRange, "%d-%d", &start, &end); err != nil { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "We don't understand your Content-Range", + } + } + b.lock.Lock() + defer b.lock.Unlock() + if start != len(b.uploads[target]) { + return ®Error{ + Status: http.StatusRequestedRangeNotSatisfiable, + Code: "BLOB_UPLOAD_UNKNOWN", + Message: "Your content range doesn't match what we have", + } + } + l := bytes.NewBuffer(b.uploads[target]) + io.Copy(l, req.Body) + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + } + + b.lock.Lock() + defer b.lock.Unlock() + if _, ok := b.uploads[target]; ok { + return ®Error{ + Status: http.StatusBadRequest, + Code: "BLOB_UPLOAD_INVALID", + Message: "Stream uploads after first write are not allowed", + } + } + + l := &bytes.Buffer{} + io.Copy(l, req.Body) + + b.uploads[target] = l.Bytes() + resp.Header().Set("Location", "/"+path.Join("v2", path.Join(elem[1:len(elem)-3]...), "blobs/uploads", target)) + resp.Header().Set("Range", fmt.Sprintf("0-%d", len(l.Bytes())-1)) + resp.WriteHeader(http.StatusNoContent) + return nil + + case http.MethodPut: + bph, ok := b.blobHandler.(blobPutHandler) + if !ok { + return regErrUnsupported + } + + if service != "uploads" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: fmt.Sprintf("PUT to /blobs must be followed by /uploads, got %s", service), + } + } + + if digest == "" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest not specified", + } + } + + b.lock.Lock() + defer b.lock.Unlock() + + h, err := v1.NewHash(digest) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + + defer req.Body.Close() + in := io.NopCloser(io.MultiReader(bytes.NewBuffer(b.uploads[target]), req.Body)) + + size := int64(verify.SizeUnknown) + if req.ContentLength > 0 { + size = int64(len(b.uploads[target])) + req.ContentLength + } + + vrc, err := verify.ReadCloser(in, size, h) + if err != nil { + return regErrInternal(err) + } + defer vrc.Close() + + if err := bph.Put(req.Context(), repo, h, vrc); err != nil { + if errors.As(err, &verify.Error{}) { + log.Printf("Digest mismatch: %v", err) + return regErrDigestMismatch + } + return regErrInternal(err) + } + + delete(b.uploads, target) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.WriteHeader(http.StatusCreated) + return nil + + case http.MethodDelete: + bdh, ok := b.blobHandler.(blobDeleteHandler) + if !ok { + return regErrUnsupported + } + + h, err := v1.NewHash(target) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", + } + } + if err := bdh.Delete(req.Context(), repo, h); err != nil { + return regErrInternal(err) + } + resp.WriteHeader(http.StatusAccepted) + return nil + + default: + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } +} diff --git a/pkg/registry/compatibility_test.go b/pkg/registry/compatibility_test.go new file mode 100644 index 0000000..eff22c2 --- /dev/null +++ b/pkg/registry/compatibility_test.go @@ -0,0 +1,63 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry_test + +import ( + "bytes" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func TestPushAndPullContainer(t *testing.T) { + s := httptest.NewServer(registry.New()) + defer s.Close() + + r := strings.TrimPrefix(s.URL, "http://") + "/foo:latest" + d, err := name.NewTag(r) + if err != nil { + t.Fatalf("Unable to create tag: %v", err) + } + + i, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("Unable to make random image: %v", err) + } + + if err := remote.Write(d, i); err != nil { + t.Fatalf("Error writing image: %v", err) + } + + ref, err := name.ParseReference(r) + if err != nil { + t.Fatalf("Error parsing tag: %v", err) + } + + ri, err := remote.Image(ref) + if err != nil { + t.Fatalf("Error reading image: %v", err) + } + + b := &bytes.Buffer{} + if err := tarball.Write(ref, ri, b); err != nil { + t.Fatalf("Error writing image to tarball: %v", err) + } +} diff --git a/pkg/registry/depcheck_test.go b/pkg/registry/depcheck_test.go new file mode 100644 index 0000000..ca0bec5 --- /dev/null +++ b/pkg/registry/depcheck_test.go @@ -0,0 +1,38 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "testing" + + "github.com/google/go-containerregistry/internal/depcheck" +) + +func TestDeps(t *testing.T) { + if testing.Short() { + t.Skip("skipping slow depcheck") + } + depcheck.AssertOnlyDependencies(t, map[string][]string{ + "github.com/google/go-containerregistry/pkg/registry": append( + depcheck.StdlibPackages(), + "github.com/google/go-containerregistry/internal/httptest", + "github.com/google/go-containerregistry/pkg/v1", + "github.com/google/go-containerregistry/pkg/v1/types", + + "github.com/google/go-containerregistry/internal/verify", + "github.com/google/go-containerregistry/internal/and", + ), + }) +} diff --git a/pkg/registry/error.go b/pkg/registry/error.go new file mode 100644 index 0000000..f8e126d --- /dev/null +++ b/pkg/registry/error.go @@ -0,0 +1,79 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "encoding/json" + "net/http" +) + +type regError struct { + Status int + Code string + Message string +} + +func (r *regError) Write(resp http.ResponseWriter) error { + resp.WriteHeader(r.Status) + + type err struct { + Code string `json:"code"` + Message string `json:"message"` + } + type wrap struct { + Errors []err `json:"errors"` + } + return json.NewEncoder(resp).Encode(wrap{ + Errors: []err{ + { + Code: r.Code, + Message: r.Message, + }, + }, + }) +} + +// regErrInternal returns an internal server error. +func regErrInternal(err error) *regError { + return ®Error{ + Status: http.StatusInternalServerError, + Code: "INTERNAL_SERVER_ERROR", + Message: err.Error(), + } +} + +var regErrBlobUnknown = ®Error{ + Status: http.StatusNotFound, + Code: "BLOB_UNKNOWN", + Message: "Unknown blob", +} + +var regErrUnsupported = ®Error{ + Status: http.StatusMethodNotAllowed, + Code: "UNSUPPORTED", + Message: "Unsupported operation", +} + +var regErrDigestMismatch = ®Error{ + Status: http.StatusBadRequest, + Code: "DIGEST_INVALID", + Message: "digest does not match contents", +} + +var regErrDigestInvalid = ®Error{ + Status: http.StatusBadRequest, + Code: "NAME_INVALID", + Message: "invalid digest", +} diff --git a/pkg/registry/manifest.go b/pkg/registry/manifest.go new file mode 100644 index 0000000..cd788f7 --- /dev/null +++ b/pkg/registry/manifest.go @@ -0,0 +1,430 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + "strconv" + "strings" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type catalog struct { + Repos []string `json:"repositories"` +} + +type listTags struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +type manifest struct { + contentType string + blob []byte +} + +type manifests struct { + // maps repo -> manifest tag/digest -> manifest + manifests map[string]map[string]manifest + lock sync.Mutex + log *log.Logger +} + +func isManifest(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "manifests" +} + +func isTags(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "tags" +} + +func isCatalog(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 2 { + return false + } + + return elems[len(elems)-1] == "_catalog" +} + +// Returns whether this url should be handled by the referrers handler +func isReferrers(req *http.Request) bool { + elems := strings.Split(req.URL.Path, "/") + elems = elems[1:] + if len(elems) < 4 { + return false + } + return elems[len(elems)-2] == "referrers" +} + +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pulling-an-image-manifest +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#pushing-an-image +func (m *manifests) handle(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + target := elem[len(elem)-1] + repo := strings.Join(elem[1:len(elem)-2], "/") + + switch req.Method { + case http.MethodGet: + m.lock.Lock() + defer m.lock.Unlock() + + c, ok := m.manifests[repo] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := c[target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + h, _, _ := v1.SHA256(bytes.NewReader(m.blob)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader(m.blob)) + return nil + + case http.MethodHead: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + m, ok := m.manifests[repo][target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + h, _, _ := v1.SHA256(bytes.NewReader(m.blob)) + resp.Header().Set("Docker-Content-Digest", h.String()) + resp.Header().Set("Content-Type", m.contentType) + resp.Header().Set("Content-Length", fmt.Sprint(len(m.blob))) + resp.WriteHeader(http.StatusOK) + return nil + + case http.MethodPut: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + m.manifests[repo] = map[string]manifest{} + } + b := &bytes.Buffer{} + io.Copy(b, req.Body) + h, _, _ := v1.SHA256(bytes.NewReader(b.Bytes())) + digest := h.String() + mf := manifest{ + blob: b.Bytes(), + contentType: req.Header.Get("Content-Type"), + } + + // If the manifest is a manifest list, check that the manifest + // list's constituent manifests are already uploaded. + // This isn't strictly required by the registry API, but some + // registries require this. + if types.MediaType(mf.contentType).IsIndex() { + im, err := v1.ParseIndexManifest(b) + if err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "MANIFEST_INVALID", + Message: err.Error(), + } + } + for _, desc := range im.Manifests { + if !desc.MediaType.IsDistributable() { + continue + } + if desc.MediaType.IsIndex() || desc.MediaType.IsImage() { + if _, found := m.manifests[repo][desc.Digest.String()]; !found { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest), + } + } + } else { + // TODO: Probably want to do an existence check for blobs. + m.log.Printf("TODO: Check blobs for %q", desc.Digest) + } + } + } + + // Allow future references by target (tag) and immutable digest. + // See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier. + m.manifests[repo][target] = mf + m.manifests[repo][digest] = mf + resp.Header().Set("Docker-Content-Digest", digest) + resp.WriteHeader(http.StatusCreated) + return nil + + case http.MethodDelete: + m.lock.Lock() + defer m.lock.Unlock() + if _, ok := m.manifests[repo]; !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + + _, ok := m.manifests[repo][target] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "MANIFEST_UNKNOWN", + Message: "Unknown manifest", + } + } + + delete(m.manifests[repo], target) + resp.WriteHeader(http.StatusAccepted) + return nil + + default: + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } +} + +func (m *manifests) handleTags(resp http.ResponseWriter, req *http.Request) *regError { + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + repo := strings.Join(elem[1:len(elem)-2], "/") + + if req.Method == "GET" { + m.lock.Lock() + defer m.lock.Unlock() + + c, ok := m.manifests[repo] + if !ok { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + + var tags []string + for tag := range c { + if !strings.Contains(tag, "sha256:") { + tags = append(tags, tag) + } + } + sort.Strings(tags) + + // https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated + // Offset using last query parameter. + if last := req.URL.Query().Get("last"); last != "" { + for i, t := range tags { + if t > last { + tags = tags[i:] + break + } + } + } + + // Limit using n query parameter. + if ns := req.URL.Query().Get("n"); ns != "" { + if n, err := strconv.Atoi(ns); err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "BAD_REQUEST", + Message: fmt.Sprintf("parsing n: %v", err), + } + } else if n < len(tags) { + tags = tags[:n] + } + } + + tagsToList := listTags{ + Name: repo, + Tags: tags, + } + + msg, _ := json.Marshal(tagsToList) + resp.Header().Set("Content-Length", fmt.Sprint(len(msg))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader([]byte(msg))) + return nil + } + + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} + +func (m *manifests) handleCatalog(resp http.ResponseWriter, req *http.Request) *regError { + query := req.URL.Query() + nStr := query.Get("n") + n := 10000 + if nStr != "" { + n, _ = strconv.Atoi(nStr) + } + + if req.Method == "GET" { + m.lock.Lock() + defer m.lock.Unlock() + + var repos []string + countRepos := 0 + // TODO: implement pagination + for key := range m.manifests { + if countRepos >= n { + break + } + countRepos++ + + repos = append(repos, key) + } + + repositoriesToList := catalog{ + Repos: repos, + } + + msg, _ := json.Marshal(repositoriesToList) + resp.Header().Set("Content-Length", fmt.Sprint(len(msg))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader([]byte(msg))) + return nil + } + + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } +} + +// TODO: implement handling of artifactType querystring +func (m *manifests) handleReferrers(resp http.ResponseWriter, req *http.Request) *regError { + // Ensure this is a GET request + if req.Method != "GET" { + return ®Error{ + Status: http.StatusBadRequest, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } + + elem := strings.Split(req.URL.Path, "/") + elem = elem[1:] + target := elem[len(elem)-1] + repo := strings.Join(elem[1:len(elem)-2], "/") + + // Validate that incoming target is a valid digest + if _, err := v1.NewHash(target); err != nil { + return ®Error{ + Status: http.StatusBadRequest, + Code: "UNSUPPORTED", + Message: "Target must be a valid digest", + } + } + + m.lock.Lock() + defer m.lock.Unlock() + + digestToManifestMap, repoExists := m.manifests[repo] + if !repoExists { + return ®Error{ + Status: http.StatusNotFound, + Code: "NAME_UNKNOWN", + Message: "Unknown name", + } + } + + im := v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Manifests: []v1.Descriptor{}, + } + for digest, manifest := range digestToManifestMap { + h, err := v1.NewHash(digest) + if err != nil { + continue + } + var refPointer struct { + Subject *v1.Descriptor `json:"subject"` + } + json.Unmarshal(manifest.blob, &refPointer) + if refPointer.Subject == nil { + continue + } + referenceDigest := refPointer.Subject.Digest + if referenceDigest.String() != target { + continue + } + // At this point, we know the current digest references the target + var imageAsArtifact struct { + Config struct { + MediaType string `json:"mediaType"` + } `json:"config"` + } + json.Unmarshal(manifest.blob, &imageAsArtifact) + im.Manifests = append(im.Manifests, v1.Descriptor{ + MediaType: types.MediaType(manifest.contentType), + Size: int64(len(manifest.blob)), + Digest: h, + ArtifactType: imageAsArtifact.Config.MediaType, + }) + } + msg, _ := json.Marshal(&im) + resp.Header().Set("Content-Length", fmt.Sprint(len(msg))) + resp.WriteHeader(http.StatusOK) + io.Copy(resp, bytes.NewReader([]byte(msg))) + return nil +} diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go new file mode 100644 index 0000000..303e6e7 --- /dev/null +++ b/pkg/registry/registry.go @@ -0,0 +1,117 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package registry implements a docker V2 registry and the OCI distribution specification. +// +// It is designed to be used anywhere a low dependency container registry is needed, with an +// initial focus on tests. +// +// Its goal is to be standards compliant and its strictness will increase over time. +// +// This is currently a low flightmiles system. It's likely quite safe to use in tests; If you're using it +// in production, please let us know how and send us CL's for integration tests. +package registry + +import ( + "log" + "net/http" + "os" +) + +type registry struct { + log *log.Logger + blobs blobs + manifests manifests + referrersEnabled bool +} + +// https://docs.docker.com/registry/spec/api/#api-version-check +// https://github.com/opencontainers/distribution-spec/blob/master/spec.md#api-version-check +func (r *registry) v2(resp http.ResponseWriter, req *http.Request) *regError { + if isBlob(req) { + return r.blobs.handle(resp, req) + } + if isManifest(req) { + return r.manifests.handle(resp, req) + } + if isTags(req) { + return r.manifests.handleTags(resp, req) + } + if isCatalog(req) { + return r.manifests.handleCatalog(resp, req) + } + if r.referrersEnabled && isReferrers(req) { + return r.manifests.handleReferrers(resp, req) + } + resp.Header().Set("Docker-Distribution-API-Version", "registry/2.0") + if req.URL.Path != "/v2/" && req.URL.Path != "/v2" { + return ®Error{ + Status: http.StatusNotFound, + Code: "METHOD_UNKNOWN", + Message: "We don't understand your method + url", + } + } + resp.WriteHeader(200) + return nil +} + +func (r *registry) root(resp http.ResponseWriter, req *http.Request) { + if rerr := r.v2(resp, req); rerr != nil { + r.log.Printf("%s %s %d %s %s", req.Method, req.URL, rerr.Status, rerr.Code, rerr.Message) + rerr.Write(resp) + return + } + r.log.Printf("%s %s", req.Method, req.URL) +} + +// New returns a handler which implements the docker registry protocol. +// It should be registered at the site root. +func New(opts ...Option) http.Handler { + r := ®istry{ + log: log.New(os.Stderr, "", log.LstdFlags), + blobs: blobs{ + blobHandler: &memHandler{m: map[string][]byte{}}, + uploads: map[string][]byte{}, + log: log.New(os.Stderr, "", log.LstdFlags), + }, + manifests: manifests{ + manifests: map[string]map[string]manifest{}, + log: log.New(os.Stderr, "", log.LstdFlags), + }, + } + for _, o := range opts { + o(r) + } + return http.HandlerFunc(r.root) +} + +// Option describes the available options +// for creating the registry. +type Option func(r *registry) + +// Logger overrides the logger used to record requests to the registry. +func Logger(l *log.Logger) Option { + return func(r *registry) { + r.log = l + r.manifests.log = l + r.blobs.log = l + } +} + +// WithReferrersSupport enables the referrers API endpoint (OCI 1.1+) +func WithReferrersSupport(enabled bool) Option { + return func(r *registry) { + r.referrersEnabled = enabled + } +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 0000000..0ee492e --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,609 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry_test + +import ( + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +const ( + weirdIndex = `{ + "manifests": [ + { + "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + "mediaType":"application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" + },{ + "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + "mediaType":"application/xml" + },{ + "digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + "mediaType":"application/vnd.oci.image.manifest.v1+json" + } + ] +}` +) + +func sha256String(s string) string { + h, _, _ := v1.SHA256(strings.NewReader(s)) + return h.Hex +} + +func TestCalls(t *testing.T) { + tcs := []struct { + Description string + + // Request / setup + URL string + Digests map[string]string + Manifests map[string]string + BlobStream map[string]string + RequestHeader map[string]string + + // Response + Code int + Header map[string]string + Method string + Body string // request body to send + Want string // response body to expect + }{ + { + Description: "/v2 returns 200", + Method: "GET", + URL: "/v2", + Code: http.StatusOK, + Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, + }, + { + Description: "/v2/ returns 200", + Method: "GET", + URL: "/v2/", + Code: http.StatusOK, + Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, + }, + { + Description: "/v2/bad returns 404", + Method: "GET", + URL: "/v2/bad", + Code: http.StatusNotFound, + Header: map[string]string{"Docker-Distribution-API-Version": "registry/2.0"}, + }, + { + Description: "GET non existent blob", + Method: "GET", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusNotFound, + }, + { + Description: "HEAD non existent blob", + Method: "HEAD", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusNotFound, + }, + { + Description: "GET bad digest", + Method: "GET", + URL: "/v2/foo/blobs/sha256:asd", + Code: http.StatusBadRequest, + }, + { + Description: "HEAD bad digest", + Method: "HEAD", + URL: "/v2/foo/blobs/sha256:asd", + Code: http.StatusBadRequest, + }, + { + Description: "bad blob verb", + Method: "FOO", + URL: "/v2/foo/blobs/sha256:asd", + Code: http.StatusBadRequest, + }, + { + Description: "GET containerless blob", + Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, + Method: "GET", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusOK, + Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + Want: "foo", + }, + { + Description: "GET blob", + Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, + Method: "GET", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusOK, + Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + Want: "foo", + }, + { + Description: "HEAD blob", + Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, + Method: "HEAD", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusOK, + Header: map[string]string{ + "Content-Length": "3", + "Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + }, + }, + { + Description: "DELETE blob", + Digests: map[string]string{"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae": "foo"}, + Method: "DELETE", + URL: "/v2/foo/blobs/sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusAccepted, + }, + { + Description: "blob url with no container", + Method: "GET", + URL: "/v2/blobs/sha256:asd", + Code: http.StatusBadRequest, + }, + { + Description: "uploadurl", + Method: "POST", + URL: "/v2/foo/blobs/uploads", + Code: http.StatusAccepted, + Header: map[string]string{"Range": "0-0"}, + }, + { + Description: "uploadurl", + Method: "POST", + URL: "/v2/foo/blobs/uploads/", + Code: http.StatusAccepted, + Header: map[string]string{"Range": "0-0"}, + }, + { + Description: "upload put missing digest", + Method: "PUT", + URL: "/v2/foo/blobs/uploads/1", + Code: http.StatusBadRequest, + }, + { + Description: "monolithic upload good digest", + Method: "POST", + URL: "/v2/foo/blobs/uploads?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusCreated, + Body: "foo", + Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + }, + { + Description: "monolithic upload bad digest", + Method: "POST", + URL: "/v2/foo/blobs/uploads?digest=sha256:fake", + Code: http.StatusBadRequest, + Body: "foo", + }, + { + Description: "upload good digest", + Method: "PUT", + URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + Code: http.StatusCreated, + Body: "foo", + Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + }, + { + Description: "upload bad digest", + Method: "PUT", + URL: "/v2/foo/blobs/uploads/1?digest=sha256:baddigest", + Code: http.StatusBadRequest, + Body: "foo", + }, + { + Description: "stream upload", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + Code: http.StatusNoContent, + Body: "foo", + Header: map[string]string{ + "Range": "0-2", + "Location": "/v2/foo/blobs/uploads/1", + }, + }, + { + Description: "stream duplicate upload", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + Code: http.StatusBadRequest, + Body: "foo", + BlobStream: map[string]string{"1": "foo"}, + }, + { + Description: "stream finish upload", + Method: "PUT", + URL: "/v2/foo/blobs/uploads/1?digest=sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", + BlobStream: map[string]string{"1": "foo"}, + Code: http.StatusCreated, + Header: map[string]string{"Docker-Content-Digest": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"}, + }, + { + Description: "get missing manifest", + Method: "GET", + URL: "/v2/foo/manifests/latest", + Code: http.StatusNotFound, + }, + { + Description: "head missing manifest", + Method: "HEAD", + URL: "/v2/foo/manifests/latest", + Code: http.StatusNotFound, + }, + { + Description: "get missing manifest good container", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "GET", + URL: "/v2/foo/manifests/bar", + Code: http.StatusNotFound, + }, + { + Description: "head missing manifest good container", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "HEAD", + URL: "/v2/foo/manifests/bar", + Code: http.StatusNotFound, + }, + { + Description: "get manifest by tag", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "GET", + URL: "/v2/foo/manifests/latest", + Code: http.StatusOK, + Want: "foo", + }, + { + Description: "get manifest by digest", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "GET", + URL: "/v2/foo/manifests/sha256:" + sha256String("foo"), + Code: http.StatusOK, + Want: "foo", + }, + { + Description: "head manifest", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "HEAD", + URL: "/v2/foo/manifests/latest", + Code: http.StatusOK, + }, + { + Description: "create manifest", + Method: "PUT", + URL: "/v2/foo/manifests/latest", + Code: http.StatusCreated, + Body: "foo", + }, + { + Description: "create index", + Method: "PUT", + URL: "/v2/foo/manifests/latest", + Code: http.StatusCreated, + Body: weirdIndex, + RequestHeader: map[string]string{ + "Content-Type": "application/vnd.oci.image.index.v1+json", + }, + Manifests: map[string]string{"foo/manifests/image": "foo"}, + }, + { + Description: "create index missing child", + Method: "PUT", + URL: "/v2/foo/manifests/latest", + Code: http.StatusNotFound, + Body: weirdIndex, + RequestHeader: map[string]string{ + "Content-Type": "application/vnd.oci.image.index.v1+json", + }, + }, + { + Description: "bad index body", + Method: "PUT", + URL: "/v2/foo/manifests/latest", + Code: http.StatusBadRequest, + Body: "foo", + RequestHeader: map[string]string{ + "Content-Type": "application/vnd.oci.image.index.v1+json", + }, + }, + { + Description: "bad manifest method", + Method: "BAR", + URL: "/v2/foo/manifests/latest", + Code: http.StatusBadRequest, + }, + { + Description: "Chunk upload start", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + RequestHeader: map[string]string{"Content-Range": "0-3"}, + Code: http.StatusNoContent, + Body: "foo", + Header: map[string]string{ + "Range": "0-2", + "Location": "/v2/foo/blobs/uploads/1", + }, + }, + { + Description: "Chunk upload bad content range", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + RequestHeader: map[string]string{"Content-Range": "0-bar"}, + Code: http.StatusRequestedRangeNotSatisfiable, + Body: "foo", + }, + { + Description: "Chunk upload overlaps previous data", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + BlobStream: map[string]string{"1": "foo"}, + RequestHeader: map[string]string{"Content-Range": "2-5"}, + Code: http.StatusRequestedRangeNotSatisfiable, + Body: "bar", + }, + { + Description: "Chunk upload after previous data", + Method: "PATCH", + URL: "/v2/foo/blobs/uploads/1", + BlobStream: map[string]string{"1": "foo"}, + RequestHeader: map[string]string{"Content-Range": "3-6"}, + Code: http.StatusNoContent, + Body: "bar", + Header: map[string]string{ + "Range": "0-5", + "Location": "/v2/foo/blobs/uploads/1", + }, + }, + { + Description: "DELETE Unknown name", + Method: "DELETE", + URL: "/v2/test/honk/manifests/latest", + Code: http.StatusNotFound, + }, + { + Description: "DELETE Unknown manifest", + Manifests: map[string]string{"honk/manifests/latest": "honk"}, + Method: "DELETE", + URL: "/v2/honk/manifests/tag-honk", + Code: http.StatusNotFound, + }, + { + Description: "DELETE existing manifest", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "DELETE", + URL: "/v2/foo/manifests/latest", + Code: http.StatusAccepted, + }, + { + Description: "DELETE existing manifest by digest", + Manifests: map[string]string{"foo/manifests/latest": "foo"}, + Method: "DELETE", + URL: "/v2/foo/manifests/sha256:" + sha256String("foo"), + Code: http.StatusAccepted, + }, + { + Description: "list tags", + Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, + Method: "GET", + URL: "/v2/foo/tags/list?n=1000", + Code: http.StatusOK, + Want: `{"name":"foo","tags":["latest","tag1"]}`, + }, + { + Description: "limit tags", + Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, + Method: "GET", + URL: "/v2/foo/tags/list?n=1", + Code: http.StatusOK, + Want: `{"name":"foo","tags":["latest"]}`, + }, + { + Description: "offset tags", + Manifests: map[string]string{"foo/manifests/latest": "foo", "foo/manifests/tag1": "foo"}, + Method: "GET", + URL: "/v2/foo/tags/list?last=latest", + Code: http.StatusOK, + Want: `{"name":"foo","tags":["tag1"]}`, + }, + { + Description: "list non existing tags", + Method: "GET", + URL: "/v2/foo/tags/list?n=1000", + Code: http.StatusNotFound, + }, + { + Description: "list repos", + Manifests: map[string]string{"foo/manifests/latest": "foo", "bar/manifests/latest": "bar"}, + Method: "GET", + URL: "/v2/_catalog?n=1000", + Code: http.StatusOK, + }, + { + Description: "fetch references", + Method: "GET", + URL: "/v2/foo/referrers/sha256:" + sha256String("foo"), + Code: http.StatusOK, + Manifests: map[string]string{ + "foo/manifests/image": "foo", + "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("foo") + "\"}}", + }, + }, + { + Description: "fetch references, subject pointing elsewhere", + Method: "GET", + URL: "/v2/foo/referrers/sha256:" + sha256String("foo"), + Code: http.StatusOK, + Manifests: map[string]string{ + "foo/manifests/image": "foo", + "foo/manifests/points-to-image": "{\"subject\": {\"digest\": \"sha256:" + sha256String("nonexistant") + "\"}}", + }, + }, + { + Description: "fetch references, no results", + Method: "GET", + URL: "/v2/foo/referrers/sha256:" + sha256String("foo"), + Code: http.StatusOK, + Manifests: map[string]string{ + "foo/manifests/image": "foo", + }, + }, + { + Description: "fetch references, missing repo", + Method: "GET", + URL: "/v2/does-not-exist/referrers/sha256:" + sha256String("foo"), + Code: http.StatusNotFound, + }, + { + Description: "fetch references, bad target (tag vs. digest)", + Method: "GET", + URL: "/v2/foo/referrers/latest", + Code: http.StatusBadRequest, + }, + { + Description: "fetch references, bad method", + Method: "POST", + URL: "/v2/foo/referrers/sha256:" + sha256String("foo"), + Code: http.StatusBadRequest, + }, + } + + for _, tc := range tcs { + + var logger *log.Logger + testf := func(t *testing.T) { + + opts := []registry.Option{registry.WithReferrersSupport(true)} + if logger != nil { + opts = append(opts, registry.Logger(logger)) + } + r := registry.New(opts...) + s := httptest.NewServer(r) + defer s.Close() + + for manifest, contents := range tc.Manifests { + u, err := url.Parse(s.URL + "/v2/" + manifest) + if err != nil { + t.Fatalf("Error parsing %q: %v", s.URL+"/v2", err) + } + req := &http.Request{ + Method: "PUT", + URL: u, + Body: io.NopCloser(strings.NewReader(contents)), + } + t.Log(req.Method, req.URL) + resp, err := s.Client().Do(req) + if err != nil { + t.Fatalf("Error uploading manifest: %v", err) + } + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Error uploading manifest got status: %d %s", resp.StatusCode, body) + } + t.Logf("created manifest with digest %v", resp.Header.Get("Docker-Content-Digest")) + } + + for digest, contents := range tc.Digests { + u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/1?digest=%s", s.URL, digest)) + if err != nil { + t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err) + } + req := &http.Request{ + Method: "PUT", + URL: u, + Body: io.NopCloser(strings.NewReader(contents)), + } + t.Log(req.Method, req.URL) + resp, err := s.Client().Do(req) + if err != nil { + t.Fatalf("Error uploading digest: %v", err) + } + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Error uploading digest got status: %d %s", resp.StatusCode, body) + } + } + + for upload, contents := range tc.BlobStream { + u, err := url.Parse(fmt.Sprintf("%s/v2/foo/blobs/uploads/%s", s.URL, upload)) + if err != nil { + t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err) + } + req := &http.Request{ + Method: "PATCH", + URL: u, + Body: io.NopCloser(strings.NewReader(contents)), + } + t.Log(req.Method, req.URL) + resp, err := s.Client().Do(req) + if err != nil { + t.Fatalf("Error streaming blob: %v", err) + } + if resp.StatusCode != http.StatusNoContent { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Error streaming blob: %d %s", resp.StatusCode, body) + } + + } + + u, err := url.Parse(s.URL + tc.URL) + if err != nil { + t.Fatalf("Error parsing %q: %v", s.URL+tc.URL, err) + } + req := &http.Request{ + Method: tc.Method, + URL: u, + Body: io.NopCloser(strings.NewReader(tc.Body)), + Header: map[string][]string{}, + } + for k, v := range tc.RequestHeader { + req.Header.Set(k, v) + } + t.Log(req.Method, req.URL) + resp, err := s.Client().Do(req) + if err != nil { + t.Fatalf("Error getting %q: %v", tc.URL, err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Errorf("Reading response body: %v", err) + } + if resp.StatusCode != tc.Code { + t.Errorf("Incorrect status code, got %d, want %d; body: %s", resp.StatusCode, tc.Code, body) + } + + for k, v := range tc.Header { + r := resp.Header.Get(k) + if r != v { + t.Errorf("Incorrect header %q received, got %q, want %q", k, r, v) + } + } + + if tc.Want != "" && string(body) != tc.Want { + t.Errorf("Incorrect response body, got %q, want %q", body, tc.Want) + } + } + t.Run(tc.Description, testf) + logger = log.New(io.Discard, "", log.Ldate) + t.Run(tc.Description+" - custom log", testf) + } +} diff --git a/pkg/registry/tls.go b/pkg/registry/tls.go new file mode 100644 index 0000000..cb2644e --- /dev/null +++ b/pkg/registry/tls.go @@ -0,0 +1,29 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry + +import ( + "net/http/httptest" + + ggcrtest "github.com/google/go-containerregistry/internal/httptest" +) + +// TLS returns an httptest server, with an http client that has been configured to +// send all requests to the returned server. The TLS certs are generated for the given domain +// which should correspond to the domain the image is stored in. +// If you need a transport, Client().Transport is correctly configured. +func TLS(domain string) (*httptest.Server, error) { + return ggcrtest.NewTLSServer(domain, New()) +} diff --git a/pkg/registry/tls_test.go b/pkg/registry/tls_test.go new file mode 100644 index 0000000..0f65b70 --- /dev/null +++ b/pkg/registry/tls_test.go @@ -0,0 +1,49 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package registry_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func TestTLS(t *testing.T) { + s, err := registry.TLS("registry.example.com") + if err != nil { + t.Fatal(err) + } + defer s.Close() + + i, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("Unable to make image: %v", err) + } + rd, err := i.Digest() + if err != nil { + t.Fatalf("Unable to get image digest: %v", err) + } + + d, err := name.NewDigest("registry.example.com/foo@" + rd.String()) + if err != nil { + t.Fatalf("Unable to parse digest: %v", err) + } + if err := remote.Write(d, i, remote.WithTransport(s.Client().Transport)); err != nil { + t.Fatalf("Unable to write image to remote: %s", err) + } +} diff --git a/pkg/v1/cache/cache.go b/pkg/v1/cache/cache.go new file mode 100644 index 0000000..31d9c93 --- /dev/null +++ b/pkg/v1/cache/cache.go @@ -0,0 +1,194 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cache provides methods to cache layers. +package cache + +import ( + "errors" + "io" + + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Cache encapsulates methods to interact with cached layers. +type Cache interface { + // Put writes the Layer to the Cache. + // + // The returned Layer should be used for future operations, since lazy + // cachers might only populate the cache when the layer is actually + // consumed. + // + // The returned layer can be consumed, and the cache entry populated, + // by calling either Compressed or Uncompressed and consuming the + // returned io.ReadCloser. + Put(v1.Layer) (v1.Layer, error) + + // Get returns the Layer cached by the given Hash, or ErrNotFound if no + // such layer was found. + Get(v1.Hash) (v1.Layer, error) + + // Delete removes the Layer with the given Hash from the Cache. + Delete(v1.Hash) error +} + +// ErrNotFound is returned by Get when no layer with the given Hash is found. +var ErrNotFound = errors.New("layer was not found") + +// Image returns a new Image which wraps the given Image, whose layers will be +// pulled from the Cache if they are found, and written to the Cache as they +// are read from the underlying Image. +func Image(i v1.Image, c Cache) v1.Image { + return &image{ + Image: i, + c: c, + } +} + +type image struct { + v1.Image + c Cache +} + +func (i *image) Layers() ([]v1.Layer, error) { + ls, err := i.Image.Layers() + if err != nil { + return nil, err + } + + out := make([]v1.Layer, len(ls)) + for idx, l := range ls { + out[idx] = &lazyLayer{inner: l, c: i.c} + } + return out, nil +} + +type lazyLayer struct { + inner v1.Layer + c Cache +} + +func (l *lazyLayer) Compressed() (io.ReadCloser, error) { + digest, err := l.inner.Digest() + if err != nil { + return nil, err + } + + if cl, err := l.c.Get(digest); err == nil { + // Layer found in the cache. + logs.Progress.Printf("Layer %s found (compressed) in cache", digest) + return cl.Compressed() + } else if !errors.Is(err, ErrNotFound) { + return nil, err + } + + // Not cached, pull and return the real layer. + logs.Progress.Printf("Layer %s not found (compressed) in cache, getting", digest) + rl, err := l.c.Put(l.inner) + if err != nil { + return nil, err + } + return rl.Compressed() +} + +func (l *lazyLayer) Uncompressed() (io.ReadCloser, error) { + diffID, err := l.inner.DiffID() + if err != nil { + return nil, err + } + if cl, err := l.c.Get(diffID); err == nil { + // Layer found in the cache. + logs.Progress.Printf("Layer %s found (uncompressed) in cache", diffID) + return cl.Uncompressed() + } else if !errors.Is(err, ErrNotFound) { + return nil, err + } + + // Not cached, pull and return the real layer. + logs.Progress.Printf("Layer %s not found (uncompressed) in cache, getting", diffID) + rl, err := l.c.Put(l.inner) + if err != nil { + return nil, err + } + return rl.Uncompressed() +} + +func (l *lazyLayer) Size() (int64, error) { return l.inner.Size() } +func (l *lazyLayer) DiffID() (v1.Hash, error) { return l.inner.DiffID() } +func (l *lazyLayer) Digest() (v1.Hash, error) { return l.inner.Digest() } +func (l *lazyLayer) MediaType() (types.MediaType, error) { return l.inner.MediaType() } + +func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) { + l, err := i.c.Get(h) + if errors.Is(err, ErrNotFound) { + // Not cached, get it and write it. + l, err := i.Image.LayerByDigest(h) + if err != nil { + return nil, err + } + return i.c.Put(l) + } + return l, err +} + +func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) { + l, err := i.c.Get(h) + if errors.Is(err, ErrNotFound) { + // Not cached, get it and write it. + l, err := i.Image.LayerByDiffID(h) + if err != nil { + return nil, err + } + return i.c.Put(l) + } + return l, err +} + +// ImageIndex returns a new ImageIndex which wraps the given ImageIndex's +// children with either Image(child, c) or ImageIndex(child, c) depending on type. +func ImageIndex(ii v1.ImageIndex, c Cache) v1.ImageIndex { + return &imageIndex{ + inner: ii, + c: c, + } +} + +type imageIndex struct { + inner v1.ImageIndex + c Cache +} + +func (ii *imageIndex) MediaType() (types.MediaType, error) { return ii.inner.MediaType() } +func (ii *imageIndex) Digest() (v1.Hash, error) { return ii.inner.Digest() } +func (ii *imageIndex) Size() (int64, error) { return ii.inner.Size() } +func (ii *imageIndex) IndexManifest() (*v1.IndexManifest, error) { return ii.inner.IndexManifest() } +func (ii *imageIndex) RawManifest() ([]byte, error) { return ii.inner.RawManifest() } + +func (ii *imageIndex) Image(h v1.Hash) (v1.Image, error) { + i, err := ii.inner.Image(h) + if err != nil { + return nil, err + } + return Image(i, ii.c), nil +} + +func (ii *imageIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + idx, err := ii.inner.ImageIndex(h) + if err != nil { + return nil, err + } + return ImageIndex(idx, ii.c), nil +} diff --git a/pkg/v1/cache/cache_test.go b/pkg/v1/cache/cache_test.go new file mode 100644 index 0000000..ee5091b --- /dev/null +++ b/pkg/v1/cache/cache_test.go @@ -0,0 +1,154 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "errors" + "io" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestImage(t *testing.T) { + img, err := random.Image(1024, 5) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + m := &memcache{map[v1.Hash]v1.Layer{}} + img = Image(img, m) + + // Validate twice to hit the cache. + if err := validate.Image(img); err != nil { + t.Errorf("Validate: %v", err) + } + if err := validate.Image(img); err != nil { + t.Errorf("Validate: %v", err) + } +} + +func TestImageIndex(t *testing.T) { + // ImageIndex with child Image and ImageIndex manifests. + ii, err := random.Index(1024, 5, 2) + if err != nil { + t.Fatalf("random.Index: %v", err) + } + iiChild, err := random.Index(1024, 5, 2) + if err != nil { + t.Fatalf("random.Index: %v", err) + } + ii = mutate.AppendManifests(ii, mutate.IndexAddendum{Add: iiChild}) + + m := &memcache{map[v1.Hash]v1.Layer{}} + ii = ImageIndex(ii, m) + + // Validate twice to hit the cache. + if err := validate.Index(ii); err != nil { + t.Errorf("Validate: %v", err) + } + if err := validate.Index(ii); err != nil { + t.Errorf("Validate: %v", err) + } +} + +func TestLayersLazy(t *testing.T) { + img, err := random.Image(1024, 5) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + m := &memcache{map[v1.Hash]v1.Layer{}} + img = Image(img, m) + + layers, err := img.Layers() + if err != nil { + t.Fatalf("img.Layers: %v", err) + } + + // After calling Layers, nothing is cached. + if got, want := len(m.m), 0; got != want { + t.Errorf("Cache has %d entries, want %d", got, want) + } + + rc, err := layers[0].Uncompressed() + if err != nil { + t.Fatalf("layer.Uncompressed: %v", err) + } + io.Copy(io.Discard, rc) + + if got, expected := len(m.m), 1; got != expected { + t.Errorf("expected %v layers in cache after reading, got %v", expected, got) + } +} + +// TestCacheShortCircuit tests that if a layer is found in the cache, +// LayerByDigest is not called in the underlying Image implementation. +func TestCacheShortCircuit(t *testing.T) { + l := &fakeLayer{} + m := &memcache{map[v1.Hash]v1.Layer{ + fakeHash: l, + }} + img := Image(&fakeImage{}, m) + + for i := 0; i < 10; i++ { + if _, err := img.LayerByDigest(fakeHash); err != nil { + t.Errorf("LayerByDigest[%d]: %v", i, err) + } + } +} + +var fakeHash = v1.Hash{Algorithm: "fake", Hex: "data"} + +type fakeLayer struct{ v1.Layer } +type fakeImage struct{ v1.Image } + +func (f *fakeImage) LayerByDigest(v1.Hash) (v1.Layer, error) { + return nil, errors.New("LayerByDigest was called") +} + +// memcache is an in-memory Cache implementation. +// +// It doesn't intend to actually write layer data, it just keeps a reference +// to the original Layer. +// +// It only assumes/considers compressed layers, and so only writes layers by +// digest. +type memcache struct { + m map[v1.Hash]v1.Layer +} + +func (m *memcache) Put(l v1.Layer) (v1.Layer, error) { + digest, err := l.Digest() + if err != nil { + return nil, err + } + m.m[digest] = l + return l, nil +} + +func (m *memcache) Get(h v1.Hash) (v1.Layer, error) { + l, found := m.m[h] + if !found { + return nil, ErrNotFound + } + return l, nil +} + +func (m *memcache) Delete(h v1.Hash) error { + delete(m.m, h) + return nil +} diff --git a/pkg/v1/cache/example_test.go b/pkg/v1/cache/example_test.go new file mode 100644 index 0000000..7f20474 --- /dev/null +++ b/pkg/v1/cache/example_test.go @@ -0,0 +1,46 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache_test + +import ( + "fmt" + "log" + "os" + + "github.com/google/go-containerregistry/pkg/v1/cache" + "github.com/google/go-containerregistry/pkg/v1/random" +) + +func ExampleImage() { + img, err := random.Image(1024*1024, 3) + if err != nil { + log.Fatal(err) + } + dir, err := os.MkdirTemp("", "") + if err != nil { + log.Fatal(err) + } + fs := cache.NewFilesystemCache(dir) + + // cached will cache layers from img using the fs cache + cached := cache.Image(img, fs) + + // Use cached as you would use img. + digest, err := cached.Digest() + if err != nil { + log.Fatal(err) + } + fmt.Println(digest) +} diff --git a/pkg/v1/cache/fs.go b/pkg/v1/cache/fs.go new file mode 100644 index 0000000..75b826e --- /dev/null +++ b/pkg/v1/cache/fs.go @@ -0,0 +1,151 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +type fscache struct { + path string +} + +// NewFilesystemCache returns a Cache implementation backed by files. +func NewFilesystemCache(path string) Cache { + return &fscache{path} +} + +func (fs *fscache) Put(l v1.Layer) (v1.Layer, error) { + digest, err := l.Digest() + if err != nil { + return nil, err + } + diffID, err := l.DiffID() + if err != nil { + return nil, err + } + return &layer{ + Layer: l, + path: fs.path, + digest: digest, + diffID: diffID, + }, nil +} + +type layer struct { + v1.Layer + path string + digest, diffID v1.Hash +} + +func (l *layer) create(h v1.Hash) (io.WriteCloser, error) { + if err := os.MkdirAll(l.path, 0700); err != nil { + return nil, err + } + return os.Create(cachepath(l.path, h)) +} + +func (l *layer) Compressed() (io.ReadCloser, error) { + f, err := l.create(l.digest) + if err != nil { + return nil, err + } + rc, err := l.Layer.Compressed() + if err != nil { + return nil, err + } + return &readcloser{ + t: io.TeeReader(rc, f), + closes: []func() error{rc.Close, f.Close}, + }, nil +} + +func (l *layer) Uncompressed() (io.ReadCloser, error) { + f, err := l.create(l.diffID) + if err != nil { + return nil, err + } + rc, err := l.Layer.Uncompressed() + if err != nil { + return nil, err + } + return &readcloser{ + t: io.TeeReader(rc, f), + closes: []func() error{rc.Close, f.Close}, + }, nil +} + +type readcloser struct { + t io.Reader + closes []func() error +} + +func (rc *readcloser) Read(b []byte) (int, error) { + return rc.t.Read(b) +} + +func (rc *readcloser) Close() error { + // Call all Close methods, even if any returned an error. Return the + // first returned error. + var err error + for _, c := range rc.closes { + lastErr := c() + if err == nil { + err = lastErr + } + } + return err +} + +func (fs *fscache) Get(h v1.Hash) (v1.Layer, error) { + l, err := tarball.LayerFromFile(cachepath(fs.path, h)) + if os.IsNotExist(err) { + return nil, ErrNotFound + } + if errors.Is(err, io.ErrUnexpectedEOF) { + // Delete and return ErrNotFound because the layer was incomplete. + if err := fs.Delete(h); err != nil { + return nil, err + } + return nil, ErrNotFound + } + return l, err +} + +func (fs *fscache) Delete(h v1.Hash) error { + err := os.Remove(cachepath(fs.path, h)) + if os.IsNotExist(err) { + return ErrNotFound + } + return err +} + +func cachepath(path string, h v1.Hash) string { + var file string + if runtime.GOOS == "windows" { + file = fmt.Sprintf("%s-%s", h.Algorithm, h.Hex) + } else { + file = h.String() + } + return filepath.Join(path, file) +} diff --git a/pkg/v1/cache/fs_test.go b/pkg/v1/cache/fs_test.go new file mode 100644 index 0000000..2e05d29 --- /dev/null +++ b/pkg/v1/cache/fs_test.go @@ -0,0 +1,213 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "errors" + "io" + "os" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestFilesystemCache(t *testing.T) { + dir := t.TempDir() + + numLayers := 5 + img, err := random.Image(10, int64(numLayers)) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + c := NewFilesystemCache(dir) + img = Image(img, c) + + // Read all the (compressed) layers to populate the cache. + ls, err := img.Layers() + if err != nil { + t.Fatalf("Layers: %v", err) + } + for i, l := range ls { + rc, err := l.Compressed() + if err != nil { + t.Fatalf("layer[%d].Compressed: %v", i, err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Fatalf("Error reading contents: %v", err) + } + rc.Close() + } + + // Check that layers exist in the fs cache. + dirEntries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if got, want := len(dirEntries), numLayers; got != want { + t.Errorf("Got %d cached files, want %d", got, want) + } + for _, de := range dirEntries { + fi, err := de.Info() + if err != nil { + t.Fatal(err) + } + if fi.Size() == 0 { + t.Errorf("Cached file %q is empty", fi.Name()) + } + } + + // Read all (uncompressed) layers, those populate the cache too. + for i, l := range ls { + rc, err := l.Uncompressed() + if err != nil { + t.Fatalf("layer[%d].Compressed: %v", i, err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Fatalf("Error reading contents: %v", err) + } + rc.Close() + } + + // Check that double the layers are present now, both compressed and + // uncompressed. + dirEntries, err = os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if got, want := len(dirEntries), numLayers*2; got != want { + t.Errorf("Got %d cached files, want %d", got, want) + } + for _, de := range dirEntries { + fi, err := de.Info() + if err != nil { + t.Fatal(err) + } + if fi.Size() == 0 { + t.Errorf("Cached file %q is empty", fi.Name()) + } + } + + // Delete a cached layer, see it disappear. + l := ls[0] + h, err := l.Digest() + if err != nil { + t.Fatalf("layer.Digest: %v", err) + } + if err := c.Delete(h); err != nil { + t.Errorf("cache.Delete: %v", err) + } + dirEntries, err = os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if got, want := len(dirEntries), numLayers*2-1; got != want { + t.Errorf("Got %d cached files, want %d", got, want) + } + + // Read the image again, see the layer reappear. + for i, l := range ls { + rc, err := l.Compressed() + if err != nil { + t.Fatalf("layer[%d].Compressed: %v", i, err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Fatalf("Error reading contents: %v", err) + } + rc.Close() + } + + // Check that layers exist in the fs cache. + dirEntries, err = os.ReadDir(dir) + if err != nil { + t.Fatalf("ReadDir: %v", err) + } + if got, want := len(dirEntries), numLayers*2; got != want { + t.Errorf("Got %d cached files, want %d", got, want) + } + for _, de := range dirEntries { + fi, err := de.Info() + if err != nil { + t.Fatal(err) + } + if fi.Size() == 0 { + t.Errorf("Cached file %q is empty", fi.Name()) + } + } +} + +func TestErrNotFound(t *testing.T) { + dir := t.TempDir() + + c := NewFilesystemCache(dir) + h := v1.Hash{Algorithm: "fake", Hex: "not-found"} + if _, err := c.Get(h); !errors.Is(err, ErrNotFound) { + t.Errorf("Get(%q): %v", h, err) + } + if err := c.Delete(h); !errors.Is(err, ErrNotFound) { + t.Errorf("Delete(%q): %v", h, err) + } +} + +func TestErrUnexpectedEOF(t *testing.T) { + dir := t.TempDir() + + // create a random layer + l, err := random.Layer(10, types.DockerLayer) + if err != nil { + t.Fatalf("random.Layer: %v", err) + } + rc, err := l.Compressed() + if err != nil { + t.Fatalf("layer.Compressed(): %v", err) + } + + h, err := l.Digest() + if err != nil { + t.Fatalf("layer.Digest(): %v", err) + } + p := cachepath(dir, h) + + // Write only the first segment of the compressed layer to produce an + // UnexpectedEOF error when reading it + buf := make([]byte, 10) + n, err := rc.Read(buf) + if err != nil { + t.Fatalf("Read(buf): %v", err) + } + if err := os.WriteFile(p, buf[:n], 0644); err != nil { + t.Fatalf("os.WriteFile(%s, buf[:%d]): %v", p, n, err) + } + + c := NewFilesystemCache(dir) + + // make sure LayerFromFile returns UnexpectedEOF + if _, err := tarball.LayerFromFile(p); !errors.Is(err, io.ErrUnexpectedEOF) { + t.Fatalf("tarball.LayerFromFile(%s): expected %v, got %v", p, io.ErrUnexpectedEOF, err) + } + + // Try to Get the layer + if _, err := c.Get(h); !errors.Is(err, ErrNotFound) { + t.Errorf("Get(%q): %v", h, err) + } + + // If we had an UnexpectedEOF and the cache deleted the broken layer no file + // should exist + if _, err := os.Stat(p); !os.IsNotExist(err) { + t.Errorf("os.Stat(%q): %v", p, err) + } +} diff --git a/pkg/v1/cache/ro.go b/pkg/v1/cache/ro.go new file mode 100644 index 0000000..028a612 --- /dev/null +++ b/pkg/v1/cache/ro.go @@ -0,0 +1,27 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import v1 "github.com/google/go-containerregistry/pkg/v1" + +// ReadOnly returns a read-only implementation of the given Cache. +// +// Put and Delete operations are a no-op. +func ReadOnly(c Cache) Cache { return &ro{Cache: c} } + +type ro struct{ Cache } + +func (ro) Put(l v1.Layer) (v1.Layer, error) { return l, nil } +func (ro) Delete(v1.Hash) error { return nil } diff --git a/pkg/v1/cache/ro_test.go b/pkg/v1/cache/ro_test.go new file mode 100644 index 0000000..4ec6b1c --- /dev/null +++ b/pkg/v1/cache/ro_test.go @@ -0,0 +1,79 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cache + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" +) + +func TestReadOnly(t *testing.T) { + m := &memcache{map[v1.Hash]v1.Layer{}} + ro := ReadOnly(m) + + // Populate the cache. + img, err := random.Image(10, 1) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + img = Image(img, m) + ls, err := img.Layers() + if err != nil { + t.Fatalf("Layers: %v", err) + } + if got, want := len(ls), 1; got != want { + t.Fatalf("Layers returned %d layers, want %d", got, want) + } + h, err := ls[0].Digest() + if err != nil { + t.Fatalf("layer.Digest: %v", err) + } + m.m[h] = ls[0] + + // Layer can be read from original cache and RO cache. + if _, err := m.Get(h); err != nil { + t.Fatalf("m.Get: %v", err) + } + if _, err := ro.Get(h); err != nil { + t.Fatalf("ro.Get: %v", err) + } + ln := len(m.m) + + // RO Put is a no-op. + if _, err := ro.Put(ls[0]); err != nil { + t.Fatalf("ro.Put: %v", err) + } + if got, want := len(m.m), ln; got != want { + t.Errorf("After Put, got %v entries, want %v", got, want) + } + + // RO Delete is a no-op. + if err := ro.Delete(h); err != nil { + t.Fatalf("ro.Delete: %v", err) + } + if got, want := len(m.m), ln; got != want { + t.Errorf("After Delete, got %v entries, want %v", got, want) + } + + // Deleting from the underlying RW cache updates RO view. + if err := m.Delete(h); err != nil { + t.Fatalf("m.Delete: %v", err) + } + if got, want := len(m.m), 0; got != want { + t.Errorf("After RW Delete, got %v entries, want %v", got, want) + } +} diff --git a/pkg/v1/config.go b/pkg/v1/config.go new file mode 100644 index 0000000..960c93b --- /dev/null +++ b/pkg/v1/config.go @@ -0,0 +1,151 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "io" + "time" +) + +// ConfigFile is the configuration file that holds the metadata describing +// how to launch a container. See: +// https://github.com/opencontainers/image-spec/blob/master/config.md +// +// docker_version and os.version are not part of the spec but included +// for backwards compatibility. +type ConfigFile struct { + Architecture string `json:"architecture"` + Author string `json:"author,omitempty"` + Container string `json:"container,omitempty"` + Created Time `json:"created,omitempty"` + DockerVersion string `json:"docker_version,omitempty"` + History []History `json:"history,omitempty"` + OS string `json:"os"` + RootFS RootFS `json:"rootfs"` + Config Config `json:"config"` + OSVersion string `json:"os.version,omitempty"` + Variant string `json:"variant,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` +} + +// Platform attempts to generates a Platform from the ConfigFile fields. +func (cf *ConfigFile) Platform() *Platform { + if cf.OS == "" && cf.Architecture == "" && cf.OSVersion == "" && cf.Variant == "" && len(cf.OSFeatures) == 0 { + return nil + } + return &Platform{ + OS: cf.OS, + Architecture: cf.Architecture, + OSVersion: cf.OSVersion, + Variant: cf.Variant, + OSFeatures: cf.OSFeatures, + } +} + +// History is one entry of a list recording how this container image was built. +type History struct { + Author string `json:"author,omitempty"` + Created Time `json:"created,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + Comment string `json:"comment,omitempty"` + EmptyLayer bool `json:"empty_layer,omitempty"` +} + +// Time is a wrapper around time.Time to help with deep copying +type Time struct { + time.Time +} + +// DeepCopyInto creates a deep-copy of the Time value. The underlying time.Time +// type is effectively immutable in the time API, so it is safe to +// copy-by-assign, despite the presence of (unexported) Pointer fields. +func (t *Time) DeepCopyInto(out *Time) { + *out = *t +} + +// RootFS holds the ordered list of file system deltas that comprise the +// container image's root filesystem. +type RootFS struct { + Type string `json:"type"` + DiffIDs []Hash `json:"diff_ids"` +} + +// HealthConfig holds configuration settings for the HEALTHCHECK feature. +type HealthConfig struct { + // Test is the test to perform to check that the container is healthy. + // An empty slice means to inherit the default. + // The options are: + // {} : inherit healthcheck + // {"NONE"} : disable healthcheck + // {"CMD", args...} : exec arguments directly + // {"CMD-SHELL", command} : run command with system's default shell + Test []string `json:",omitempty"` + + // Zero means to inherit. Durations are expressed as integer nanoseconds. + Interval time.Duration `json:",omitempty"` // Interval is the time to wait between checks. + Timeout time.Duration `json:",omitempty"` // Timeout is the time to wait before considering the check to have hung. + StartPeriod time.Duration `json:",omitempty"` // The start period for the container to initialize before the retries starts to count down. + + // Retries is the number of consecutive failures needed to consider a container as unhealthy. + // Zero means inherit. + Retries int `json:",omitempty"` +} + +// Config is a submessage of the config file described as: +// +// The execution parameters which SHOULD be used as a base when running +// a container using the image. +// +// The names of the fields in this message are chosen to reflect the JSON +// payload of the Config as defined here: +// https://git.io/vrAET +// and +// https://github.com/opencontainers/image-spec/blob/master/config.md +type Config struct { + AttachStderr bool `json:"AttachStderr,omitempty"` + AttachStdin bool `json:"AttachStdin,omitempty"` + AttachStdout bool `json:"AttachStdout,omitempty"` + Cmd []string `json:"Cmd,omitempty"` + Healthcheck *HealthConfig `json:"Healthcheck,omitempty"` + Domainname string `json:"Domainname,omitempty"` + Entrypoint []string `json:"Entrypoint,omitempty"` + Env []string `json:"Env,omitempty"` + Hostname string `json:"Hostname,omitempty"` + Image string `json:"Image,omitempty"` + Labels map[string]string `json:"Labels,omitempty"` + OnBuild []string `json:"OnBuild,omitempty"` + OpenStdin bool `json:"OpenStdin,omitempty"` + StdinOnce bool `json:"StdinOnce,omitempty"` + Tty bool `json:"Tty,omitempty"` + User string `json:"User,omitempty"` + Volumes map[string]struct{} `json:"Volumes,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty"` + ExposedPorts map[string]struct{} `json:"ExposedPorts,omitempty"` + ArgsEscaped bool `json:"ArgsEscaped,omitempty"` + NetworkDisabled bool `json:"NetworkDisabled,omitempty"` + MacAddress string `json:"MacAddress,omitempty"` + StopSignal string `json:"StopSignal,omitempty"` + Shell []string `json:"Shell,omitempty"` +} + +// ParseConfigFile parses the io.Reader's contents into a ConfigFile. +func ParseConfigFile(r io.Reader) (*ConfigFile, error) { + cf := ConfigFile{} + if err := json.NewDecoder(r).Decode(&cf); err != nil { + return nil, err + } + return &cf, nil +} diff --git a/pkg/v1/config_test.go b/pkg/v1/config_test.go new file mode 100644 index 0000000..6e190bf --- /dev/null +++ b/pkg/v1/config_test.go @@ -0,0 +1,38 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseConfig(t *testing.T) { + got, err := ParseConfigFile(strings.NewReader("{}")) + if err != nil { + t.Fatal(err) + } + want := ConfigFile{} + + if diff := cmp.Diff(want, *got); diff != "" { + t.Errorf("ParseConfigFile({}); (-want +got) %s", diff) + } + + if got, err := ParseConfigFile(strings.NewReader("{")); err == nil { + t.Errorf("expected error, got: %v", got) + } +} diff --git a/pkg/v1/daemon/README.md b/pkg/v1/daemon/README.md new file mode 100644 index 0000000..74fc3a8 --- /dev/null +++ b/pkg/v1/daemon/README.md @@ -0,0 +1,11 @@ +# `daemon` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/daemon) + +The `daemon` package enables reading/writing images from/to the docker daemon. + +It is not fully fleshed out, but is useful for interoperability, see various issues: + +* https://github.com/google/go-containerregistry/issues/205 +* https://github.com/google/go-containerregistry/issues/552 +* https://github.com/google/go-containerregistry/issues/627 diff --git a/pkg/v1/daemon/doc.go b/pkg/v1/daemon/doc.go new file mode 100644 index 0000000..ac05d96 --- /dev/null +++ b/pkg/v1/daemon/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package daemon provides facilities for reading/writing v1.Image from/to +// a running daemon. +package daemon diff --git a/pkg/v1/daemon/image.go b/pkg/v1/daemon/image.go new file mode 100644 index 0000000..55ba833 --- /dev/null +++ b/pkg/v1/daemon/image.go @@ -0,0 +1,203 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "bytes" + "context" + "io" + "sync" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type image struct { + ref name.Reference + opener *imageOpener + tarballImage v1.Image + id *v1.Hash + + once sync.Once + err error +} + +type imageOpener struct { + ref name.Reference + ctx context.Context + + buffered bool + client Client + + once sync.Once + bytes []byte + err error +} + +func (i *imageOpener) saveImage() (io.ReadCloser, error) { + return i.client.ImageSave(i.ctx, []string{i.ref.Name()}) +} + +func (i *imageOpener) bufferedOpener() (io.ReadCloser, error) { + // Store the tarball in memory and return a new reader into the bytes each time we need to access something. + i.once.Do(func() { + i.bytes, i.err = func() ([]byte, error) { + rc, err := i.saveImage() + if err != nil { + return nil, err + } + defer rc.Close() + + return io.ReadAll(rc) + }() + }) + + // Wrap the bytes in a ReadCloser so it looks like an opened file. + return io.NopCloser(bytes.NewReader(i.bytes)), i.err +} + +func (i *imageOpener) opener() tarball.Opener { + if i.buffered { + return i.bufferedOpener + } + + // To avoid storing the tarball in memory, do a save every time we need to access something. + return i.saveImage +} + +// Image provides access to an image reference from the Docker daemon, +// applying functional options to the underlying imageOpener before +// resolving the reference into a v1.Image. +func Image(ref name.Reference, options ...Option) (v1.Image, error) { + o, err := makeOptions(options...) + if err != nil { + return nil, err + } + + i := &imageOpener{ + ref: ref, + buffered: o.buffered, + client: o.client, + ctx: o.ctx, + } + + img := &image{ + ref: ref, + opener: i, + } + + // Eagerly fetch Image ID to ensure it actually exists. + // https://github.com/google/go-containerregistry/issues/1186 + id, err := img.ConfigName() + if err != nil { + return nil, err + } + img.id = &id + + return img, nil +} + +func (i *image) initialize() error { + // Don't re-initialize tarball if already initialized. + if i.tarballImage == nil { + i.once.Do(func() { + i.tarballImage, i.err = tarball.Image(i.opener.opener(), nil) + }) + } + return i.err +} + +func (i *image) Layers() ([]v1.Layer, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.Layers() +} + +func (i *image) MediaType() (types.MediaType, error) { + if err := i.initialize(); err != nil { + return "", err + } + return i.tarballImage.MediaType() +} + +func (i *image) Size() (int64, error) { + if err := i.initialize(); err != nil { + return 0, err + } + return i.tarballImage.Size() +} + +func (i *image) ConfigName() (v1.Hash, error) { + if i.id != nil { + return *i.id, nil + } + res, _, err := i.opener.client.ImageInspectWithRaw(i.opener.ctx, i.ref.String()) + if err != nil { + return v1.Hash{}, err + } + return v1.NewHash(res.ID) +} + +func (i *image) ConfigFile() (*v1.ConfigFile, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.ConfigFile() +} + +func (i *image) RawConfigFile() ([]byte, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.RawConfigFile() +} + +func (i *image) Digest() (v1.Hash, error) { + if err := i.initialize(); err != nil { + return v1.Hash{}, err + } + return i.tarballImage.Digest() +} + +func (i *image) Manifest() (*v1.Manifest, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.Manifest() +} + +func (i *image) RawManifest() ([]byte, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.RawManifest() +} + +func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.LayerByDigest(h) +} + +func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) { + if err := i.initialize(); err != nil { + return nil, err + } + return i.tarballImage.LayerByDiffID(h) +} diff --git a/pkg/v1/daemon/image_test.go b/pkg/v1/daemon/image_test.go new file mode 100644 index 0000000..6456832 --- /dev/null +++ b/pkg/v1/daemon/image_test.go @@ -0,0 +1,159 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +var imagePath = "../tarball/testdata/test_image_1.tar" + +type MockClient struct { + Client + path string + negotiated bool + + wantCtx context.Context + + loadErr error + loadBody io.ReadCloser + + saveErr error + saveBody io.ReadCloser +} + +func (m *MockClient) NegotiateAPIVersion(ctx context.Context) { + m.negotiated = true +} + +func (m *MockClient) ImageSave(_ context.Context, _ []string) (io.ReadCloser, error) { + if !m.negotiated { + return nil, errors.New("you forgot to call NegotiateAPIVersion before calling ImageSave") + } + + if m.path != "" { + return os.Open(m.path) + } + + return m.saveBody, m.saveErr +} + +func (m *MockClient) ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error) { + return types.ImageInspect{ + ID: "sha256:6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e", + }, nil, nil +} + +func TestImage(t *testing.T) { + for _, tc := range []struct { + name string + buffered bool + client *MockClient + wantResponse string + wantErr string + }{{ + name: "success", + client: &MockClient{ + path: imagePath, + }, + }, { + name: "save err", + client: &MockClient{ + saveBody: io.NopCloser(strings.NewReader("Loaded")), + saveErr: fmt.Errorf("locked and loaded"), + }, + wantErr: "locked and loaded", + }, { + name: "read err", + client: &MockClient{ + saveBody: io.NopCloser(&errReader{fmt.Errorf("goodbye, world")}), + }, + wantErr: "goodbye, world", + }} { + run := func(t *testing.T) { + opts := []Option{WithClient(tc.client)} + if tc.buffered { + opts = append(opts, WithBufferedOpener()) + } else { + opts = append(opts, WithUnbufferedOpener()) + } + img, err := tarball.ImageFromPath(imagePath, nil) + if err != nil { + t.Fatalf("error loading test image: %s", err) + } + + tag, err := name.NewTag("unused", name.WeakValidation) + if err != nil { + t.Fatalf("error creating test name: %s", err) + } + + dmn, err := Image(tag, opts...) + if err != nil { + if tc.wantErr == "" { + t.Errorf("Error loading daemon image: %s", err) + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr) + } + return + } + err = compare.Images(img, dmn) + if err != nil { + if tc.wantErr == "" { + t.Errorf("compare.Images: %v", err) + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr) + } + } + + err = validate.Image(dmn) + if err != nil { + if tc.wantErr == "" { + t.Errorf("validate.Image: %v", err) + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("wanted %s to contain %s", err.Error(), tc.wantErr) + } + } + } + + tc.buffered = true + t.Run(tc.name+" buffered", run) + + tc.buffered = false + t.Run(tc.name+" unbuffered", run) + } +} + +func TestImageDefaultClient(t *testing.T) { + wantErr := fmt.Errorf("bad client") + defaultClient = func() (Client, error) { + return nil, wantErr + } + + if _, err := Image(name.MustParseReference("unused")); !errors.Is(err, wantErr) { + t.Errorf("Image(): want %v; got %v", wantErr, err) + } +} diff --git a/pkg/v1/daemon/options.go b/pkg/v1/daemon/options.go new file mode 100644 index 0000000..e8a5a1e --- /dev/null +++ b/pkg/v1/daemon/options.go @@ -0,0 +1,103 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "context" + "io" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" +) + +// ImageOption is an alias for Option. +// Deprecated: Use Option instead. +type ImageOption Option + +// Option is a functional option for daemon operations. +type Option func(*options) + +type options struct { + ctx context.Context + client Client + buffered bool +} + +var defaultClient = func() (Client, error) { + return client.NewClientWithOpts(client.FromEnv) +} + +func makeOptions(opts ...Option) (*options, error) { + o := &options{ + buffered: true, + ctx: context.Background(), + } + for _, opt := range opts { + opt(o) + } + + if o.client == nil { + client, err := defaultClient() + if err != nil { + return nil, err + } + o.client = client + } + o.client.NegotiateAPIVersion(o.ctx) + + return o, nil +} + +// WithBufferedOpener buffers the image. +func WithBufferedOpener() Option { + return func(o *options) { + o.buffered = true + } +} + +// WithUnbufferedOpener streams the image to avoid buffering. +func WithUnbufferedOpener() Option { + return func(o *options) { + o.buffered = false + } +} + +// WithClient is a functional option to allow injecting a docker client. +// +// By default, github.com/docker/docker/client.FromEnv is used. +func WithClient(client Client) Option { + return func(o *options) { + o.client = client + } +} + +// WithContext is a functional option to pass through a context.Context. +// +// By default, context.Background() is used. +func WithContext(ctx context.Context) Option { + return func(o *options) { + o.ctx = ctx + } +} + +// Client represents the subset of a docker client that the daemon +// package uses. +type Client interface { + NegotiateAPIVersion(ctx context.Context) + ImageSave(context.Context, []string) (io.ReadCloser, error) + ImageLoad(context.Context, io.Reader, bool) (types.ImageLoadResponse, error) + ImageTag(context.Context, string, string) error + ImageInspectWithRaw(context.Context, string) (types.ImageInspect, []byte, error) +} diff --git a/pkg/v1/daemon/write.go b/pkg/v1/daemon/write.go new file mode 100644 index 0000000..48186f6 --- /dev/null +++ b/pkg/v1/daemon/write.go @@ -0,0 +1,60 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "fmt" + "io" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +// Tag adds a tag to an already existent image. +func Tag(src, dest name.Tag, options ...Option) error { + o, err := makeOptions(options...) + if err != nil { + return err + } + + return o.client.ImageTag(o.ctx, src.String(), dest.String()) +} + +// Write saves the image into the daemon as the given tag. +func Write(tag name.Tag, img v1.Image, options ...Option) (string, error) { + o, err := makeOptions(options...) + if err != nil { + return "", err + } + + pr, pw := io.Pipe() + go func() { + pw.CloseWithError(tarball.Write(tag, img, pw)) + }() + + // write the image in docker save format first, then load it + resp, err := o.client.ImageLoad(o.ctx, pr, false) + if err != nil { + return "", fmt.Errorf("error loading image: %w", err) + } + defer resp.Body.Close() + b, err := io.ReadAll(resp.Body) + response := string(b) + if err != nil { + return response, fmt.Errorf("error reading load response body: %w", err) + } + return response, nil +} diff --git a/pkg/v1/daemon/write_test.go b/pkg/v1/daemon/write_test.go new file mode 100644 index 0000000..0e5495c --- /dev/null +++ b/pkg/v1/daemon/write_test.go @@ -0,0 +1,159 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package daemon + +import ( + "context" + "errors" + "fmt" + "io" + "strings" + "testing" + + "github.com/docker/docker/api/types" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +type errReader struct { + err error +} + +func (r *errReader) Read(p []byte) (int, error) { + return 0, r.err +} + +func (m *MockClient) ImageLoad(ctx context.Context, r io.Reader, _ bool) (types.ImageLoadResponse, error) { + if !m.negotiated { + return types.ImageLoadResponse{}, errors.New("you forgot to call NegotiateAPIVersion before calling ImageLoad") + } + if m.wantCtx != nil && m.wantCtx != ctx { + return types.ImageLoadResponse{}, fmt.Errorf("ImageLoad: wrong context") + } + + _, _ = io.Copy(io.Discard, r) + return types.ImageLoadResponse{ + Body: m.loadBody, + }, m.loadErr +} + +func (m *MockClient) ImageTag(ctx context.Context, source, target string) error { + if !m.negotiated { + return errors.New("you forgot to call NegotiateAPIVersion before calling ImageTag") + } + if m.wantCtx != nil && m.wantCtx != ctx { + return fmt.Errorf("ImageTag: wrong context") + } + return nil +} + +func TestWriteImage(t *testing.T) { + for _, tc := range []struct { + name string + client *MockClient + wantResponse string + wantErr string + }{{ + name: "success", + client: &MockClient{ + loadBody: io.NopCloser(strings.NewReader("Loaded")), + }, + wantResponse: "Loaded", + }, { + name: "load err", + client: &MockClient{ + loadBody: io.NopCloser(strings.NewReader("Loaded")), + loadErr: fmt.Errorf("locked and loaded"), + }, + wantErr: "locked and loaded", + }, { + name: "read err", + client: &MockClient{ + loadBody: io.NopCloser(&errReader{fmt.Errorf("goodbye, world")}), + }, + wantErr: "goodbye, world", + }} { + t.Run(tc.name, func(t *testing.T) { + image, err := tarball.ImageFromPath("../tarball/testdata/test_image_1.tar", nil) + if err != nil { + t.Errorf("Error loading image: %v", err.Error()) + } + tag, err := name.NewTag("test_image_2:latest") + if err != nil { + t.Fatal(err) + } + response, err := Write(tag, image, WithClient(tc.client)) + if tc.wantErr == "" { + if err != nil { + t.Errorf("Error writing image tar: %s", err.Error()) + } + } else { + if err == nil { + t.Errorf("expected err") + } else if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("Error writing image tar: wanted %s to contain %s", err.Error(), tc.wantErr) + } + } + if !strings.Contains(response, tc.wantResponse) { + t.Errorf("Error loading image. Response: %s", response) + } + + dst, err := name.NewTag("hello:world") + if err != nil { + t.Fatal(err) + } + if err := Tag(tag, dst, WithClient(tc.client)); err != nil { + t.Errorf("Error tagging image: %v", err) + } + }) + } +} + +func TestWriteDefaultClient(t *testing.T) { + wantErr := fmt.Errorf("bad client") + defaultClient = func() (Client, error) { + return nil, wantErr + } + + tag, err := name.NewTag("test_image_2:latest") + if err != nil { + t.Fatal(err) + } + + if _, err := Write(tag, empty.Image); !errors.Is(err, wantErr) { + t.Errorf("Write(): want %v; got %v", wantErr, err) + } + + if err := Tag(tag, tag); !errors.Is(err, wantErr) { + t.Errorf("Tag(): want %v; got %v", wantErr, err) + } + + // Cover default client init and ctx use as well. + ctx := context.TODO() + defaultClient = func() (Client, error) { + return &MockClient{ + loadBody: io.NopCloser(strings.NewReader("Loaded")), + wantCtx: ctx, + }, nil + } + if err := Tag(tag, tag, WithContext(ctx)); err != nil { + t.Fatal(err) + } + if _, err := Write(tag, empty.Image, WithContext(ctx)); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/v1/doc.go b/pkg/v1/doc.go new file mode 100644 index 0000000..7a84736 --- /dev/null +++ b/pkg/v1/doc.go @@ -0,0 +1,18 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// +k8s:deepcopy-gen=package + +// Package v1 defines structured types for OCI v1 images +package v1 diff --git a/pkg/v1/empty/README.md b/pkg/v1/empty/README.md new file mode 100644 index 0000000..8663a83 --- /dev/null +++ b/pkg/v1/empty/README.md @@ -0,0 +1,8 @@ +# `empty` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/empty?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/empty) + +The empty packages provides an empty base for constructing a `v1.Image` or `v1.ImageIndex`. +This is especially useful when paired with the [`mutate`](/pkg/v1/mutate) package, +see [`mutate.Append`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#Append) +and [`mutate.AppendManifests`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate#AppendManifests). diff --git a/pkg/v1/empty/doc.go b/pkg/v1/empty/doc.go new file mode 100644 index 0000000..1a521e9 --- /dev/null +++ b/pkg/v1/empty/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package empty provides an implementation of v1.Image equivalent to "FROM scratch". +package empty diff --git a/pkg/v1/empty/image.go b/pkg/v1/empty/image.go new file mode 100644 index 0000000..c58a06c --- /dev/null +++ b/pkg/v1/empty/image.go @@ -0,0 +1,52 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package empty + +import ( + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Image is a singleton empty image, think: FROM scratch. +var Image, _ = partial.UncompressedToImage(emptyImage{}) + +type emptyImage struct{} + +// MediaType implements partial.UncompressedImageCore. +func (i emptyImage) MediaType() (types.MediaType, error) { + return types.DockerManifestSchema2, nil +} + +// RawConfigFile implements partial.UncompressedImageCore. +func (i emptyImage) RawConfigFile() ([]byte, error) { + return partial.RawConfigFile(i) +} + +// ConfigFile implements v1.Image. +func (i emptyImage) ConfigFile() (*v1.ConfigFile, error) { + return &v1.ConfigFile{ + RootFS: v1.RootFS{ + // Some clients check this. + Type: "layers", + }, + }, nil +} + +func (i emptyImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) { + return nil, fmt.Errorf("LayerByDiffID(%s): empty image", h) +} diff --git a/pkg/v1/empty/image_test.go b/pkg/v1/empty/image_test.go new file mode 100644 index 0000000..c9204d9 --- /dev/null +++ b/pkg/v1/empty/image_test.go @@ -0,0 +1,48 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package empty + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestImage(t *testing.T) { + if err := validate.Image(Image); err != nil { + t.Fatalf("validate.Image(empty.Image) = %v", err) + } +} + +func TestManifestAndConfig(t *testing.T) { + manifest, err := Image.Manifest() + if err != nil { + t.Fatalf("Error loading manifest: %v", err) + } + if got, want := len(manifest.Layers), 0; got != want { + t.Fatalf("num layers; got %v, want %v", got, want) + } + + config, err := Image.ConfigFile() + if err != nil { + t.Fatalf("Error loading config file: %v", err) + } + if got, want := len(config.RootFS.DiffIDs), 0; got != want { + t.Fatalf("num diff ids; got %v, want %v", got, want) + } + if got, want := config.RootFS.Type, "layers"; got != want { + t.Fatalf("rootfs type; got %v, want %v", got, want) + } +} diff --git a/pkg/v1/empty/index.go b/pkg/v1/empty/index.go new file mode 100644 index 0000000..1066535 --- /dev/null +++ b/pkg/v1/empty/index.go @@ -0,0 +1,64 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package empty + +import ( + "encoding/json" + "errors" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Index is a singleton empty index, think: FROM scratch. +var Index = emptyIndex{} + +type emptyIndex struct{} + +func (i emptyIndex) MediaType() (types.MediaType, error) { + return types.OCIImageIndex, nil +} + +func (i emptyIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i emptyIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i emptyIndex) IndexManifest() (*v1.IndexManifest, error) { + return base(), nil +} + +func (i emptyIndex) RawManifest() ([]byte, error) { + return json.Marshal(base()) +} + +func (i emptyIndex) Image(v1.Hash) (v1.Image, error) { + return nil, errors.New("empty index") +} + +func (i emptyIndex) ImageIndex(v1.Hash) (v1.ImageIndex, error) { + return nil, errors.New("empty index") +} + +func base() *v1.IndexManifest { + return &v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + } +} diff --git a/pkg/v1/empty/index_test.go b/pkg/v1/empty/index_test.go new file mode 100644 index 0000000..dd99e10 --- /dev/null +++ b/pkg/v1/empty/index_test.go @@ -0,0 +1,40 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package empty + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestIndex(t *testing.T) { + if err := validate.Index(Index); err != nil { + t.Fatalf("validate.Index(empty.Index) = %v", err) + } + + if mt, err := Index.MediaType(); err != nil || mt != types.OCIImageIndex { + t.Errorf("empty.Index.MediaType() = %v, %v", mt, err) + } + + if _, err := Index.Image(v1.Hash{}); err == nil { + t.Errorf("empty.Index.Image() should always fail") + } + if _, err := Index.ImageIndex(v1.Hash{}); err == nil { + t.Errorf("empty.Index.ImageIndex() should always fail") + } +} diff --git a/pkg/v1/fake/image.go b/pkg/v1/fake/image.go new file mode 100644 index 0000000..f95ac61 --- /dev/null +++ b/pkg/v1/fake/image.go @@ -0,0 +1,826 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fake + +import ( + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type FakeImage struct { + ConfigFileStub func() (*v1.ConfigFile, error) + configFileMutex sync.RWMutex + configFileArgsForCall []struct { + } + configFileReturns struct { + result1 *v1.ConfigFile + result2 error + } + configFileReturnsOnCall map[int]struct { + result1 *v1.ConfigFile + result2 error + } + ConfigNameStub func() (v1.Hash, error) + configNameMutex sync.RWMutex + configNameArgsForCall []struct { + } + configNameReturns struct { + result1 v1.Hash + result2 error + } + configNameReturnsOnCall map[int]struct { + result1 v1.Hash + result2 error + } + DigestStub func() (v1.Hash, error) + digestMutex sync.RWMutex + digestArgsForCall []struct { + } + digestReturns struct { + result1 v1.Hash + result2 error + } + digestReturnsOnCall map[int]struct { + result1 v1.Hash + result2 error + } + LayerByDiffIDStub func(v1.Hash) (v1.Layer, error) + layerByDiffIDMutex sync.RWMutex + layerByDiffIDArgsForCall []struct { + arg1 v1.Hash + } + layerByDiffIDReturns struct { + result1 v1.Layer + result2 error + } + layerByDiffIDReturnsOnCall map[int]struct { + result1 v1.Layer + result2 error + } + LayerByDigestStub func(v1.Hash) (v1.Layer, error) + layerByDigestMutex sync.RWMutex + layerByDigestArgsForCall []struct { + arg1 v1.Hash + } + layerByDigestReturns struct { + result1 v1.Layer + result2 error + } + layerByDigestReturnsOnCall map[int]struct { + result1 v1.Layer + result2 error + } + LayersStub func() ([]v1.Layer, error) + layersMutex sync.RWMutex + layersArgsForCall []struct { + } + layersReturns struct { + result1 []v1.Layer + result2 error + } + layersReturnsOnCall map[int]struct { + result1 []v1.Layer + result2 error + } + ManifestStub func() (*v1.Manifest, error) + manifestMutex sync.RWMutex + manifestArgsForCall []struct { + } + manifestReturns struct { + result1 *v1.Manifest + result2 error + } + manifestReturnsOnCall map[int]struct { + result1 *v1.Manifest + result2 error + } + MediaTypeStub func() (types.MediaType, error) + mediaTypeMutex sync.RWMutex + mediaTypeArgsForCall []struct { + } + mediaTypeReturns struct { + result1 types.MediaType + result2 error + } + mediaTypeReturnsOnCall map[int]struct { + result1 types.MediaType + result2 error + } + RawConfigFileStub func() ([]byte, error) + rawConfigFileMutex sync.RWMutex + rawConfigFileArgsForCall []struct { + } + rawConfigFileReturns struct { + result1 []byte + result2 error + } + rawConfigFileReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + RawManifestStub func() ([]byte, error) + rawManifestMutex sync.RWMutex + rawManifestArgsForCall []struct { + } + rawManifestReturns struct { + result1 []byte + result2 error + } + rawManifestReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + SizeStub func() (int64, error) + sizeMutex sync.RWMutex + sizeArgsForCall []struct { + } + sizeReturns struct { + result1 int64 + result2 error + } + sizeReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeImage) ConfigFile() (*v1.ConfigFile, error) { + fake.configFileMutex.Lock() + ret, specificReturn := fake.configFileReturnsOnCall[len(fake.configFileArgsForCall)] + fake.configFileArgsForCall = append(fake.configFileArgsForCall, struct { + }{}) + stub := fake.ConfigFileStub + fakeReturns := fake.configFileReturns + fake.recordInvocation("ConfigFile", []interface{}{}) + fake.configFileMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) ConfigFileCallCount() int { + fake.configFileMutex.RLock() + defer fake.configFileMutex.RUnlock() + return len(fake.configFileArgsForCall) +} + +func (fake *FakeImage) ConfigFileCalls(stub func() (*v1.ConfigFile, error)) { + fake.configFileMutex.Lock() + defer fake.configFileMutex.Unlock() + fake.ConfigFileStub = stub +} + +func (fake *FakeImage) ConfigFileReturns(result1 *v1.ConfigFile, result2 error) { + fake.configFileMutex.Lock() + defer fake.configFileMutex.Unlock() + fake.ConfigFileStub = nil + fake.configFileReturns = struct { + result1 *v1.ConfigFile + result2 error + }{result1, result2} +} + +func (fake *FakeImage) ConfigFileReturnsOnCall(i int, result1 *v1.ConfigFile, result2 error) { + fake.configFileMutex.Lock() + defer fake.configFileMutex.Unlock() + fake.ConfigFileStub = nil + if fake.configFileReturnsOnCall == nil { + fake.configFileReturnsOnCall = make(map[int]struct { + result1 *v1.ConfigFile + result2 error + }) + } + fake.configFileReturnsOnCall[i] = struct { + result1 *v1.ConfigFile + result2 error + }{result1, result2} +} + +func (fake *FakeImage) ConfigName() (v1.Hash, error) { + fake.configNameMutex.Lock() + ret, specificReturn := fake.configNameReturnsOnCall[len(fake.configNameArgsForCall)] + fake.configNameArgsForCall = append(fake.configNameArgsForCall, struct { + }{}) + stub := fake.ConfigNameStub + fakeReturns := fake.configNameReturns + fake.recordInvocation("ConfigName", []interface{}{}) + fake.configNameMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) ConfigNameCallCount() int { + fake.configNameMutex.RLock() + defer fake.configNameMutex.RUnlock() + return len(fake.configNameArgsForCall) +} + +func (fake *FakeImage) ConfigNameCalls(stub func() (v1.Hash, error)) { + fake.configNameMutex.Lock() + defer fake.configNameMutex.Unlock() + fake.ConfigNameStub = stub +} + +func (fake *FakeImage) ConfigNameReturns(result1 v1.Hash, result2 error) { + fake.configNameMutex.Lock() + defer fake.configNameMutex.Unlock() + fake.ConfigNameStub = nil + fake.configNameReturns = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImage) ConfigNameReturnsOnCall(i int, result1 v1.Hash, result2 error) { + fake.configNameMutex.Lock() + defer fake.configNameMutex.Unlock() + fake.ConfigNameStub = nil + if fake.configNameReturnsOnCall == nil { + fake.configNameReturnsOnCall = make(map[int]struct { + result1 v1.Hash + result2 error + }) + } + fake.configNameReturnsOnCall[i] = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImage) Digest() (v1.Hash, error) { + fake.digestMutex.Lock() + ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)] + fake.digestArgsForCall = append(fake.digestArgsForCall, struct { + }{}) + stub := fake.DigestStub + fakeReturns := fake.digestReturns + fake.recordInvocation("Digest", []interface{}{}) + fake.digestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) DigestCallCount() int { + fake.digestMutex.RLock() + defer fake.digestMutex.RUnlock() + return len(fake.digestArgsForCall) +} + +func (fake *FakeImage) DigestCalls(stub func() (v1.Hash, error)) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = stub +} + +func (fake *FakeImage) DigestReturns(result1 v1.Hash, result2 error) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = nil + fake.digestReturns = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImage) DigestReturnsOnCall(i int, result1 v1.Hash, result2 error) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = nil + if fake.digestReturnsOnCall == nil { + fake.digestReturnsOnCall = make(map[int]struct { + result1 v1.Hash + result2 error + }) + } + fake.digestReturnsOnCall[i] = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImage) LayerByDiffID(arg1 v1.Hash) (v1.Layer, error) { + fake.layerByDiffIDMutex.Lock() + ret, specificReturn := fake.layerByDiffIDReturnsOnCall[len(fake.layerByDiffIDArgsForCall)] + fake.layerByDiffIDArgsForCall = append(fake.layerByDiffIDArgsForCall, struct { + arg1 v1.Hash + }{arg1}) + stub := fake.LayerByDiffIDStub + fakeReturns := fake.layerByDiffIDReturns + fake.recordInvocation("LayerByDiffID", []interface{}{arg1}) + fake.layerByDiffIDMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) LayerByDiffIDCallCount() int { + fake.layerByDiffIDMutex.RLock() + defer fake.layerByDiffIDMutex.RUnlock() + return len(fake.layerByDiffIDArgsForCall) +} + +func (fake *FakeImage) LayerByDiffIDCalls(stub func(v1.Hash) (v1.Layer, error)) { + fake.layerByDiffIDMutex.Lock() + defer fake.layerByDiffIDMutex.Unlock() + fake.LayerByDiffIDStub = stub +} + +func (fake *FakeImage) LayerByDiffIDArgsForCall(i int) v1.Hash { + fake.layerByDiffIDMutex.RLock() + defer fake.layerByDiffIDMutex.RUnlock() + argsForCall := fake.layerByDiffIDArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImage) LayerByDiffIDReturns(result1 v1.Layer, result2 error) { + fake.layerByDiffIDMutex.Lock() + defer fake.layerByDiffIDMutex.Unlock() + fake.LayerByDiffIDStub = nil + fake.layerByDiffIDReturns = struct { + result1 v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) LayerByDiffIDReturnsOnCall(i int, result1 v1.Layer, result2 error) { + fake.layerByDiffIDMutex.Lock() + defer fake.layerByDiffIDMutex.Unlock() + fake.LayerByDiffIDStub = nil + if fake.layerByDiffIDReturnsOnCall == nil { + fake.layerByDiffIDReturnsOnCall = make(map[int]struct { + result1 v1.Layer + result2 error + }) + } + fake.layerByDiffIDReturnsOnCall[i] = struct { + result1 v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) LayerByDigest(arg1 v1.Hash) (v1.Layer, error) { + fake.layerByDigestMutex.Lock() + ret, specificReturn := fake.layerByDigestReturnsOnCall[len(fake.layerByDigestArgsForCall)] + fake.layerByDigestArgsForCall = append(fake.layerByDigestArgsForCall, struct { + arg1 v1.Hash + }{arg1}) + stub := fake.LayerByDigestStub + fakeReturns := fake.layerByDigestReturns + fake.recordInvocation("LayerByDigest", []interface{}{arg1}) + fake.layerByDigestMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) LayerByDigestCallCount() int { + fake.layerByDigestMutex.RLock() + defer fake.layerByDigestMutex.RUnlock() + return len(fake.layerByDigestArgsForCall) +} + +func (fake *FakeImage) LayerByDigestCalls(stub func(v1.Hash) (v1.Layer, error)) { + fake.layerByDigestMutex.Lock() + defer fake.layerByDigestMutex.Unlock() + fake.LayerByDigestStub = stub +} + +func (fake *FakeImage) LayerByDigestArgsForCall(i int) v1.Hash { + fake.layerByDigestMutex.RLock() + defer fake.layerByDigestMutex.RUnlock() + argsForCall := fake.layerByDigestArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImage) LayerByDigestReturns(result1 v1.Layer, result2 error) { + fake.layerByDigestMutex.Lock() + defer fake.layerByDigestMutex.Unlock() + fake.LayerByDigestStub = nil + fake.layerByDigestReturns = struct { + result1 v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) LayerByDigestReturnsOnCall(i int, result1 v1.Layer, result2 error) { + fake.layerByDigestMutex.Lock() + defer fake.layerByDigestMutex.Unlock() + fake.LayerByDigestStub = nil + if fake.layerByDigestReturnsOnCall == nil { + fake.layerByDigestReturnsOnCall = make(map[int]struct { + result1 v1.Layer + result2 error + }) + } + fake.layerByDigestReturnsOnCall[i] = struct { + result1 v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) Layers() ([]v1.Layer, error) { + fake.layersMutex.Lock() + ret, specificReturn := fake.layersReturnsOnCall[len(fake.layersArgsForCall)] + fake.layersArgsForCall = append(fake.layersArgsForCall, struct { + }{}) + stub := fake.LayersStub + fakeReturns := fake.layersReturns + fake.recordInvocation("Layers", []interface{}{}) + fake.layersMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) LayersCallCount() int { + fake.layersMutex.RLock() + defer fake.layersMutex.RUnlock() + return len(fake.layersArgsForCall) +} + +func (fake *FakeImage) LayersCalls(stub func() ([]v1.Layer, error)) { + fake.layersMutex.Lock() + defer fake.layersMutex.Unlock() + fake.LayersStub = stub +} + +func (fake *FakeImage) LayersReturns(result1 []v1.Layer, result2 error) { + fake.layersMutex.Lock() + defer fake.layersMutex.Unlock() + fake.LayersStub = nil + fake.layersReturns = struct { + result1 []v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) LayersReturnsOnCall(i int, result1 []v1.Layer, result2 error) { + fake.layersMutex.Lock() + defer fake.layersMutex.Unlock() + fake.LayersStub = nil + if fake.layersReturnsOnCall == nil { + fake.layersReturnsOnCall = make(map[int]struct { + result1 []v1.Layer + result2 error + }) + } + fake.layersReturnsOnCall[i] = struct { + result1 []v1.Layer + result2 error + }{result1, result2} +} + +func (fake *FakeImage) Manifest() (*v1.Manifest, error) { + fake.manifestMutex.Lock() + ret, specificReturn := fake.manifestReturnsOnCall[len(fake.manifestArgsForCall)] + fake.manifestArgsForCall = append(fake.manifestArgsForCall, struct { + }{}) + stub := fake.ManifestStub + fakeReturns := fake.manifestReturns + fake.recordInvocation("Manifest", []interface{}{}) + fake.manifestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) ManifestCallCount() int { + fake.manifestMutex.RLock() + defer fake.manifestMutex.RUnlock() + return len(fake.manifestArgsForCall) +} + +func (fake *FakeImage) ManifestCalls(stub func() (*v1.Manifest, error)) { + fake.manifestMutex.Lock() + defer fake.manifestMutex.Unlock() + fake.ManifestStub = stub +} + +func (fake *FakeImage) ManifestReturns(result1 *v1.Manifest, result2 error) { + fake.manifestMutex.Lock() + defer fake.manifestMutex.Unlock() + fake.ManifestStub = nil + fake.manifestReturns = struct { + result1 *v1.Manifest + result2 error + }{result1, result2} +} + +func (fake *FakeImage) ManifestReturnsOnCall(i int, result1 *v1.Manifest, result2 error) { + fake.manifestMutex.Lock() + defer fake.manifestMutex.Unlock() + fake.ManifestStub = nil + if fake.manifestReturnsOnCall == nil { + fake.manifestReturnsOnCall = make(map[int]struct { + result1 *v1.Manifest + result2 error + }) + } + fake.manifestReturnsOnCall[i] = struct { + result1 *v1.Manifest + result2 error + }{result1, result2} +} + +func (fake *FakeImage) MediaType() (types.MediaType, error) { + fake.mediaTypeMutex.Lock() + ret, specificReturn := fake.mediaTypeReturnsOnCall[len(fake.mediaTypeArgsForCall)] + fake.mediaTypeArgsForCall = append(fake.mediaTypeArgsForCall, struct { + }{}) + stub := fake.MediaTypeStub + fakeReturns := fake.mediaTypeReturns + fake.recordInvocation("MediaType", []interface{}{}) + fake.mediaTypeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) MediaTypeCallCount() int { + fake.mediaTypeMutex.RLock() + defer fake.mediaTypeMutex.RUnlock() + return len(fake.mediaTypeArgsForCall) +} + +func (fake *FakeImage) MediaTypeCalls(stub func() (types.MediaType, error)) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = stub +} + +func (fake *FakeImage) MediaTypeReturns(result1 types.MediaType, result2 error) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = nil + fake.mediaTypeReturns = struct { + result1 types.MediaType + result2 error + }{result1, result2} +} + +func (fake *FakeImage) MediaTypeReturnsOnCall(i int, result1 types.MediaType, result2 error) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = nil + if fake.mediaTypeReturnsOnCall == nil { + fake.mediaTypeReturnsOnCall = make(map[int]struct { + result1 types.MediaType + result2 error + }) + } + fake.mediaTypeReturnsOnCall[i] = struct { + result1 types.MediaType + result2 error + }{result1, result2} +} + +func (fake *FakeImage) RawConfigFile() ([]byte, error) { + fake.rawConfigFileMutex.Lock() + ret, specificReturn := fake.rawConfigFileReturnsOnCall[len(fake.rawConfigFileArgsForCall)] + fake.rawConfigFileArgsForCall = append(fake.rawConfigFileArgsForCall, struct { + }{}) + stub := fake.RawConfigFileStub + fakeReturns := fake.rawConfigFileReturns + fake.recordInvocation("RawConfigFile", []interface{}{}) + fake.rawConfigFileMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) RawConfigFileCallCount() int { + fake.rawConfigFileMutex.RLock() + defer fake.rawConfigFileMutex.RUnlock() + return len(fake.rawConfigFileArgsForCall) +} + +func (fake *FakeImage) RawConfigFileCalls(stub func() ([]byte, error)) { + fake.rawConfigFileMutex.Lock() + defer fake.rawConfigFileMutex.Unlock() + fake.RawConfigFileStub = stub +} + +func (fake *FakeImage) RawConfigFileReturns(result1 []byte, result2 error) { + fake.rawConfigFileMutex.Lock() + defer fake.rawConfigFileMutex.Unlock() + fake.RawConfigFileStub = nil + fake.rawConfigFileReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImage) RawConfigFileReturnsOnCall(i int, result1 []byte, result2 error) { + fake.rawConfigFileMutex.Lock() + defer fake.rawConfigFileMutex.Unlock() + fake.RawConfigFileStub = nil + if fake.rawConfigFileReturnsOnCall == nil { + fake.rawConfigFileReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.rawConfigFileReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImage) RawManifest() ([]byte, error) { + fake.rawManifestMutex.Lock() + ret, specificReturn := fake.rawManifestReturnsOnCall[len(fake.rawManifestArgsForCall)] + fake.rawManifestArgsForCall = append(fake.rawManifestArgsForCall, struct { + }{}) + stub := fake.RawManifestStub + fakeReturns := fake.rawManifestReturns + fake.recordInvocation("RawManifest", []interface{}{}) + fake.rawManifestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) RawManifestCallCount() int { + fake.rawManifestMutex.RLock() + defer fake.rawManifestMutex.RUnlock() + return len(fake.rawManifestArgsForCall) +} + +func (fake *FakeImage) RawManifestCalls(stub func() ([]byte, error)) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = stub +} + +func (fake *FakeImage) RawManifestReturns(result1 []byte, result2 error) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = nil + fake.rawManifestReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImage) RawManifestReturnsOnCall(i int, result1 []byte, result2 error) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = nil + if fake.rawManifestReturnsOnCall == nil { + fake.rawManifestReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.rawManifestReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImage) Size() (int64, error) { + fake.sizeMutex.Lock() + ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)] + fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct { + }{}) + stub := fake.SizeStub + fakeReturns := fake.sizeReturns + fake.recordInvocation("Size", []interface{}{}) + fake.sizeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImage) SizeCallCount() int { + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + return len(fake.sizeArgsForCall) +} + +func (fake *FakeImage) SizeCalls(stub func() (int64, error)) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = stub +} + +func (fake *FakeImage) SizeReturns(result1 int64, result2 error) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + fake.sizeReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeImage) SizeReturnsOnCall(i int, result1 int64, result2 error) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + if fake.sizeReturnsOnCall == nil { + fake.sizeReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.sizeReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeImage) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.configFileMutex.RLock() + defer fake.configFileMutex.RUnlock() + fake.configNameMutex.RLock() + defer fake.configNameMutex.RUnlock() + fake.digestMutex.RLock() + defer fake.digestMutex.RUnlock() + fake.layerByDiffIDMutex.RLock() + defer fake.layerByDiffIDMutex.RUnlock() + fake.layerByDigestMutex.RLock() + defer fake.layerByDigestMutex.RUnlock() + fake.layersMutex.RLock() + defer fake.layersMutex.RUnlock() + fake.manifestMutex.RLock() + defer fake.manifestMutex.RUnlock() + fake.mediaTypeMutex.RLock() + defer fake.mediaTypeMutex.RUnlock() + fake.rawConfigFileMutex.RLock() + defer fake.rawConfigFileMutex.RUnlock() + fake.rawManifestMutex.RLock() + defer fake.rawManifestMutex.RUnlock() + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeImage) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ v1.Image = new(FakeImage) diff --git a/pkg/v1/fake/index.go b/pkg/v1/fake/index.go new file mode 100644 index 0000000..8c66a98 --- /dev/null +++ b/pkg/v1/fake/index.go @@ -0,0 +1,546 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fake + +import ( + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type FakeImageIndex struct { + DigestStub func() (v1.Hash, error) + digestMutex sync.RWMutex + digestArgsForCall []struct { + } + digestReturns struct { + result1 v1.Hash + result2 error + } + digestReturnsOnCall map[int]struct { + result1 v1.Hash + result2 error + } + ImageStub func(v1.Hash) (v1.Image, error) + imageMutex sync.RWMutex + imageArgsForCall []struct { + arg1 v1.Hash + } + imageReturns struct { + result1 v1.Image + result2 error + } + imageReturnsOnCall map[int]struct { + result1 v1.Image + result2 error + } + ImageIndexStub func(v1.Hash) (v1.ImageIndex, error) + imageIndexMutex sync.RWMutex + imageIndexArgsForCall []struct { + arg1 v1.Hash + } + imageIndexReturns struct { + result1 v1.ImageIndex + result2 error + } + imageIndexReturnsOnCall map[int]struct { + result1 v1.ImageIndex + result2 error + } + IndexManifestStub func() (*v1.IndexManifest, error) + indexManifestMutex sync.RWMutex + indexManifestArgsForCall []struct { + } + indexManifestReturns struct { + result1 *v1.IndexManifest + result2 error + } + indexManifestReturnsOnCall map[int]struct { + result1 *v1.IndexManifest + result2 error + } + MediaTypeStub func() (types.MediaType, error) + mediaTypeMutex sync.RWMutex + mediaTypeArgsForCall []struct { + } + mediaTypeReturns struct { + result1 types.MediaType + result2 error + } + mediaTypeReturnsOnCall map[int]struct { + result1 types.MediaType + result2 error + } + RawManifestStub func() ([]byte, error) + rawManifestMutex sync.RWMutex + rawManifestArgsForCall []struct { + } + rawManifestReturns struct { + result1 []byte + result2 error + } + rawManifestReturnsOnCall map[int]struct { + result1 []byte + result2 error + } + SizeStub func() (int64, error) + sizeMutex sync.RWMutex + sizeArgsForCall []struct { + } + sizeReturns struct { + result1 int64 + result2 error + } + sizeReturnsOnCall map[int]struct { + result1 int64 + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeImageIndex) Digest() (v1.Hash, error) { + fake.digestMutex.Lock() + ret, specificReturn := fake.digestReturnsOnCall[len(fake.digestArgsForCall)] + fake.digestArgsForCall = append(fake.digestArgsForCall, struct { + }{}) + stub := fake.DigestStub + fakeReturns := fake.digestReturns + fake.recordInvocation("Digest", []interface{}{}) + fake.digestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) DigestCallCount() int { + fake.digestMutex.RLock() + defer fake.digestMutex.RUnlock() + return len(fake.digestArgsForCall) +} + +func (fake *FakeImageIndex) DigestCalls(stub func() (v1.Hash, error)) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = stub +} + +func (fake *FakeImageIndex) DigestReturns(result1 v1.Hash, result2 error) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = nil + fake.digestReturns = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) DigestReturnsOnCall(i int, result1 v1.Hash, result2 error) { + fake.digestMutex.Lock() + defer fake.digestMutex.Unlock() + fake.DigestStub = nil + if fake.digestReturnsOnCall == nil { + fake.digestReturnsOnCall = make(map[int]struct { + result1 v1.Hash + result2 error + }) + } + fake.digestReturnsOnCall[i] = struct { + result1 v1.Hash + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) Image(arg1 v1.Hash) (v1.Image, error) { + fake.imageMutex.Lock() + ret, specificReturn := fake.imageReturnsOnCall[len(fake.imageArgsForCall)] + fake.imageArgsForCall = append(fake.imageArgsForCall, struct { + arg1 v1.Hash + }{arg1}) + stub := fake.ImageStub + fakeReturns := fake.imageReturns + fake.recordInvocation("Image", []interface{}{arg1}) + fake.imageMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) ImageCallCount() int { + fake.imageMutex.RLock() + defer fake.imageMutex.RUnlock() + return len(fake.imageArgsForCall) +} + +func (fake *FakeImageIndex) ImageCalls(stub func(v1.Hash) (v1.Image, error)) { + fake.imageMutex.Lock() + defer fake.imageMutex.Unlock() + fake.ImageStub = stub +} + +func (fake *FakeImageIndex) ImageArgsForCall(i int) v1.Hash { + fake.imageMutex.RLock() + defer fake.imageMutex.RUnlock() + argsForCall := fake.imageArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImageIndex) ImageReturns(result1 v1.Image, result2 error) { + fake.imageMutex.Lock() + defer fake.imageMutex.Unlock() + fake.ImageStub = nil + fake.imageReturns = struct { + result1 v1.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) ImageReturnsOnCall(i int, result1 v1.Image, result2 error) { + fake.imageMutex.Lock() + defer fake.imageMutex.Unlock() + fake.ImageStub = nil + if fake.imageReturnsOnCall == nil { + fake.imageReturnsOnCall = make(map[int]struct { + result1 v1.Image + result2 error + }) + } + fake.imageReturnsOnCall[i] = struct { + result1 v1.Image + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) ImageIndex(arg1 v1.Hash) (v1.ImageIndex, error) { + fake.imageIndexMutex.Lock() + ret, specificReturn := fake.imageIndexReturnsOnCall[len(fake.imageIndexArgsForCall)] + fake.imageIndexArgsForCall = append(fake.imageIndexArgsForCall, struct { + arg1 v1.Hash + }{arg1}) + stub := fake.ImageIndexStub + fakeReturns := fake.imageIndexReturns + fake.recordInvocation("ImageIndex", []interface{}{arg1}) + fake.imageIndexMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) ImageIndexCallCount() int { + fake.imageIndexMutex.RLock() + defer fake.imageIndexMutex.RUnlock() + return len(fake.imageIndexArgsForCall) +} + +func (fake *FakeImageIndex) ImageIndexCalls(stub func(v1.Hash) (v1.ImageIndex, error)) { + fake.imageIndexMutex.Lock() + defer fake.imageIndexMutex.Unlock() + fake.ImageIndexStub = stub +} + +func (fake *FakeImageIndex) ImageIndexArgsForCall(i int) v1.Hash { + fake.imageIndexMutex.RLock() + defer fake.imageIndexMutex.RUnlock() + argsForCall := fake.imageIndexArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeImageIndex) ImageIndexReturns(result1 v1.ImageIndex, result2 error) { + fake.imageIndexMutex.Lock() + defer fake.imageIndexMutex.Unlock() + fake.ImageIndexStub = nil + fake.imageIndexReturns = struct { + result1 v1.ImageIndex + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) ImageIndexReturnsOnCall(i int, result1 v1.ImageIndex, result2 error) { + fake.imageIndexMutex.Lock() + defer fake.imageIndexMutex.Unlock() + fake.ImageIndexStub = nil + if fake.imageIndexReturnsOnCall == nil { + fake.imageIndexReturnsOnCall = make(map[int]struct { + result1 v1.ImageIndex + result2 error + }) + } + fake.imageIndexReturnsOnCall[i] = struct { + result1 v1.ImageIndex + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) IndexManifest() (*v1.IndexManifest, error) { + fake.indexManifestMutex.Lock() + ret, specificReturn := fake.indexManifestReturnsOnCall[len(fake.indexManifestArgsForCall)] + fake.indexManifestArgsForCall = append(fake.indexManifestArgsForCall, struct { + }{}) + stub := fake.IndexManifestStub + fakeReturns := fake.indexManifestReturns + fake.recordInvocation("IndexManifest", []interface{}{}) + fake.indexManifestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) IndexManifestCallCount() int { + fake.indexManifestMutex.RLock() + defer fake.indexManifestMutex.RUnlock() + return len(fake.indexManifestArgsForCall) +} + +func (fake *FakeImageIndex) IndexManifestCalls(stub func() (*v1.IndexManifest, error)) { + fake.indexManifestMutex.Lock() + defer fake.indexManifestMutex.Unlock() + fake.IndexManifestStub = stub +} + +func (fake *FakeImageIndex) IndexManifestReturns(result1 *v1.IndexManifest, result2 error) { + fake.indexManifestMutex.Lock() + defer fake.indexManifestMutex.Unlock() + fake.IndexManifestStub = nil + fake.indexManifestReturns = struct { + result1 *v1.IndexManifest + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) IndexManifestReturnsOnCall(i int, result1 *v1.IndexManifest, result2 error) { + fake.indexManifestMutex.Lock() + defer fake.indexManifestMutex.Unlock() + fake.IndexManifestStub = nil + if fake.indexManifestReturnsOnCall == nil { + fake.indexManifestReturnsOnCall = make(map[int]struct { + result1 *v1.IndexManifest + result2 error + }) + } + fake.indexManifestReturnsOnCall[i] = struct { + result1 *v1.IndexManifest + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) MediaType() (types.MediaType, error) { + fake.mediaTypeMutex.Lock() + ret, specificReturn := fake.mediaTypeReturnsOnCall[len(fake.mediaTypeArgsForCall)] + fake.mediaTypeArgsForCall = append(fake.mediaTypeArgsForCall, struct { + }{}) + stub := fake.MediaTypeStub + fakeReturns := fake.mediaTypeReturns + fake.recordInvocation("MediaType", []interface{}{}) + fake.mediaTypeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) MediaTypeCallCount() int { + fake.mediaTypeMutex.RLock() + defer fake.mediaTypeMutex.RUnlock() + return len(fake.mediaTypeArgsForCall) +} + +func (fake *FakeImageIndex) MediaTypeCalls(stub func() (types.MediaType, error)) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = stub +} + +func (fake *FakeImageIndex) MediaTypeReturns(result1 types.MediaType, result2 error) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = nil + fake.mediaTypeReturns = struct { + result1 types.MediaType + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) MediaTypeReturnsOnCall(i int, result1 types.MediaType, result2 error) { + fake.mediaTypeMutex.Lock() + defer fake.mediaTypeMutex.Unlock() + fake.MediaTypeStub = nil + if fake.mediaTypeReturnsOnCall == nil { + fake.mediaTypeReturnsOnCall = make(map[int]struct { + result1 types.MediaType + result2 error + }) + } + fake.mediaTypeReturnsOnCall[i] = struct { + result1 types.MediaType + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) RawManifest() ([]byte, error) { + fake.rawManifestMutex.Lock() + ret, specificReturn := fake.rawManifestReturnsOnCall[len(fake.rawManifestArgsForCall)] + fake.rawManifestArgsForCall = append(fake.rawManifestArgsForCall, struct { + }{}) + stub := fake.RawManifestStub + fakeReturns := fake.rawManifestReturns + fake.recordInvocation("RawManifest", []interface{}{}) + fake.rawManifestMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) RawManifestCallCount() int { + fake.rawManifestMutex.RLock() + defer fake.rawManifestMutex.RUnlock() + return len(fake.rawManifestArgsForCall) +} + +func (fake *FakeImageIndex) RawManifestCalls(stub func() ([]byte, error)) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = stub +} + +func (fake *FakeImageIndex) RawManifestReturns(result1 []byte, result2 error) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = nil + fake.rawManifestReturns = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) RawManifestReturnsOnCall(i int, result1 []byte, result2 error) { + fake.rawManifestMutex.Lock() + defer fake.rawManifestMutex.Unlock() + fake.RawManifestStub = nil + if fake.rawManifestReturnsOnCall == nil { + fake.rawManifestReturnsOnCall = make(map[int]struct { + result1 []byte + result2 error + }) + } + fake.rawManifestReturnsOnCall[i] = struct { + result1 []byte + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) Size() (int64, error) { + fake.sizeMutex.Lock() + ret, specificReturn := fake.sizeReturnsOnCall[len(fake.sizeArgsForCall)] + fake.sizeArgsForCall = append(fake.sizeArgsForCall, struct { + }{}) + stub := fake.SizeStub + fakeReturns := fake.sizeReturns + fake.recordInvocation("Size", []interface{}{}) + fake.sizeMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeImageIndex) SizeCallCount() int { + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + return len(fake.sizeArgsForCall) +} + +func (fake *FakeImageIndex) SizeCalls(stub func() (int64, error)) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = stub +} + +func (fake *FakeImageIndex) SizeReturns(result1 int64, result2 error) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + fake.sizeReturns = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) SizeReturnsOnCall(i int, result1 int64, result2 error) { + fake.sizeMutex.Lock() + defer fake.sizeMutex.Unlock() + fake.SizeStub = nil + if fake.sizeReturnsOnCall == nil { + fake.sizeReturnsOnCall = make(map[int]struct { + result1 int64 + result2 error + }) + } + fake.sizeReturnsOnCall[i] = struct { + result1 int64 + result2 error + }{result1, result2} +} + +func (fake *FakeImageIndex) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.digestMutex.RLock() + defer fake.digestMutex.RUnlock() + fake.imageMutex.RLock() + defer fake.imageMutex.RUnlock() + fake.imageIndexMutex.RLock() + defer fake.imageIndexMutex.RUnlock() + fake.indexManifestMutex.RLock() + defer fake.indexManifestMutex.RUnlock() + fake.mediaTypeMutex.RLock() + defer fake.mediaTypeMutex.RUnlock() + fake.rawManifestMutex.RLock() + defer fake.rawManifestMutex.RUnlock() + fake.sizeMutex.RLock() + defer fake.sizeMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeImageIndex) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ v1.ImageIndex = new(FakeImageIndex) diff --git a/pkg/v1/google/README.md b/pkg/v1/google/README.md new file mode 100644 index 0000000..7cd8971 --- /dev/null +++ b/pkg/v1/google/README.md @@ -0,0 +1,7 @@ +# `google` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/google) + +The `google` package provides: +* Some google-specific authentication methods. +* Some [GCR](gcr.io)-specific listing methods. diff --git a/pkg/v1/google/auth.go b/pkg/v1/google/auth.go new file mode 100644 index 0000000..11ae397 --- /dev/null +++ b/pkg/v1/google/auth.go @@ -0,0 +1,179 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os/exec" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "golang.org/x/oauth2" + googauth "golang.org/x/oauth2/google" +) + +const cloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform" + +// GetGcloudCmd is exposed so we can test this. +var GetGcloudCmd = func() *exec.Cmd { + // This is odd, but basically what docker-credential-gcr does. + // + // config-helper is undocumented, but it's purportedly the only supported way + // of accessing tokens (`gcloud auth print-access-token` is discouraged). + // + // --force-auth-refresh means we are getting a token that is valid for about + // an hour (we reuse it until it's expired). + return exec.Command("gcloud", "config", "config-helper", "--force-auth-refresh", "--format=json(credential)") +} + +// NewEnvAuthenticator returns an authn.Authenticator that generates access +// tokens from the environment we're running in. +// +// See: https://godoc.org/golang.org/x/oauth2/google#FindDefaultCredentials +func NewEnvAuthenticator() (authn.Authenticator, error) { + ts, err := googauth.DefaultTokenSource(context.Background(), cloudPlatformScope) + if err != nil { + return nil, err + } + + token, err := ts.Token() + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil +} + +// NewGcloudAuthenticator returns an oauth2.TokenSource that generates access +// tokens by shelling out to the gcloud sdk. +func NewGcloudAuthenticator() (authn.Authenticator, error) { + if _, err := exec.LookPath("gcloud"); err != nil { + // gcloud is not available, fall back to anonymous + logs.Warn.Println("gcloud binary not found") + return authn.Anonymous, nil + } + + ts := gcloudSource{GetGcloudCmd} + + // Attempt to fetch a token to ensure gcloud is installed and we can run it. + token, err := ts.Token() + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(token, ts)}, nil +} + +// NewJSONKeyAuthenticator returns a Basic authenticator which uses Service Account +// as a way of authenticating with Google Container Registry. +// More information: https://cloud.google.com/container-registry/docs/advanced-authentication#json_key_file +func NewJSONKeyAuthenticator(serviceAccountJSON string) authn.Authenticator { + return &authn.Basic{ + Username: "_json_key", + Password: serviceAccountJSON, + } +} + +// NewTokenAuthenticator returns an oauth2.TokenSource that generates access +// tokens by using the Google SDK to produce JWT tokens from a Service Account. +// More information: https://godoc.org/golang.org/x/oauth2/google#JWTAccessTokenSourceFromJSON +func NewTokenAuthenticator(serviceAccountJSON string, scope string) (authn.Authenticator, error) { + ts, err := googauth.JWTAccessTokenSourceFromJSON([]byte(serviceAccountJSON), scope) + if err != nil { + return nil, err + } + + return &tokenSourceAuth{oauth2.ReuseTokenSource(nil, ts)}, nil +} + +// NewTokenSourceAuthenticator converts an oauth2.TokenSource into an authn.Authenticator. +func NewTokenSourceAuthenticator(ts oauth2.TokenSource) authn.Authenticator { + return &tokenSourceAuth{ts} +} + +// tokenSourceAuth turns an oauth2.TokenSource into an authn.Authenticator. +type tokenSourceAuth struct { + oauth2.TokenSource +} + +// Authorization implements authn.Authenticator. +func (tsa *tokenSourceAuth) Authorization() (*authn.AuthConfig, error) { + token, err := tsa.Token() + if err != nil { + return nil, err + } + + return &authn.AuthConfig{ + Username: "_token", + Password: token.AccessToken, + }, nil +} + +// gcloudOutput represents the output of the gcloud command we invoke. +// +// `gcloud config config-helper --format=json(credential)` looks something like: +// +// { +// "credential": { +// "access_token": "supersecretaccesstoken", +// "token_expiry": "2018-12-02T04:08:13Z" +// } +// } +type gcloudOutput struct { + Credential struct { + AccessToken string `json:"access_token"` + TokenExpiry string `json:"token_expiry"` + } `json:"credential"` +} + +type gcloudSource struct { + // This is passed in so that we mock out gcloud and test Token. + exec func() *exec.Cmd +} + +// Token implements oauath2.TokenSource. +func (gs gcloudSource) Token() (*oauth2.Token, error) { + cmd := gs.exec() + var out bytes.Buffer + cmd.Stdout = &out + + // Don't attempt to interpret stderr, just pass it through. + cmd.Stderr = logs.Warn.Writer() + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("error executing `gcloud config config-helper`: %w", err) + } + + creds := gcloudOutput{} + if err := json.Unmarshal(out.Bytes(), &creds); err != nil { + return nil, fmt.Errorf("failed to parse `gcloud config config-helper` output: %w", err) + } + + expiry, err := time.Parse(time.RFC3339, creds.Credential.TokenExpiry) + if err != nil { + return nil, fmt.Errorf("failed to parse gcloud token expiry: %w", err) + } + + token := oauth2.Token{ + AccessToken: creds.Credential.AccessToken, + Expiry: expiry, + } + + return &token, nil +} diff --git a/pkg/v1/google/auth_test.go b/pkg/v1/google/auth_test.go new file mode 100644 index 0000000..d2974ff --- /dev/null +++ b/pkg/v1/google/auth_test.go @@ -0,0 +1,270 @@ +//go:build !arm64 +// +build !arm64 + +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "golang.org/x/oauth2" +) + +const ( + // Fails to parse as JSON at all. + badoutput = "" + + // Fails to parse token_expiry format. + badexpiry = ` +{ + "credential": { + "access_token": "mytoken", + "token_expiry": "most-definitely-not-a-date" + } +}` + + // Expires in 6,000 years. Hopefully nobody is using software then. + success = ` +{ + "credential": { + "access_token": "mytoken", + "token_expiry": "8018-12-02T04:08:13Z" + } +}` +) + +// We'll invoke ourselves with a special environment variable in order to mock +// out the gcloud dependency of gcloudSource. The exec package does this, too. +// +// See: https://www.joeshaw.org/testing-with-os-exec-and-testmain/ +// +// TODO(#908): This doesn't work on arm64 or darwin for some reason. +func TestMain(m *testing.M) { + switch os.Getenv("GO_TEST_MODE") { + case "": + // Normal test mode + os.Exit(m.Run()) + + case "error": + // Makes cmd.Run() return an error. + os.Exit(2) + + case "badoutput": + // Makes the gcloudOutput Unmarshaler fail. + fmt.Println(badoutput) + + case "badexpiry": + // Makes the token_expiry time parser fail. + fmt.Println(badexpiry) + + case "success": + // Returns a seemingly valid token. + fmt.Println(success) + } +} + +func newGcloudCmdMock(env string) func() *exec.Cmd { + return func() *exec.Cmd { + cmd := exec.Command(os.Args[0]) + cmd.Env = []string{fmt.Sprintf("GO_TEST_MODE=%s", env)} + return cmd + } +} + +func TestGcloudErrors(t *testing.T) { + cases := []struct { + env string + + // Just look for the prefix because we can't control other packages' errors. + wantPrefix string + }{{ + env: "error", + wantPrefix: "error executing `gcloud config config-helper`:", + }, { + env: "badoutput", + wantPrefix: "failed to parse `gcloud config config-helper` output:", + }, { + env: "badexpiry", + wantPrefix: "failed to parse gcloud token expiry:", + }} + + for _, tc := range cases { + t.Run(tc.env, func(t *testing.T) { + GetGcloudCmd = newGcloudCmdMock(tc.env) + + if _, err := NewGcloudAuthenticator(); err == nil { + t.Errorf("wanted error, got nil") + } else if got := err.Error(); !strings.HasPrefix(got, tc.wantPrefix) { + t.Errorf("wanted error prefix %q, got %q", tc.wantPrefix, got) + } + }) + } +} + +func TestGcloudSuccess(t *testing.T) { + // Stupid coverage to make sure it doesn't panic. + var b bytes.Buffer + logs.Debug.SetOutput(&b) + + GetGcloudCmd = newGcloudCmdMock("success") + + auth, err := NewGcloudAuthenticator() + if err != nil { + t.Fatalf("NewGcloudAuthenticator got error %v", err) + } + + token, err := auth.Authorization() + if err != nil { + t.Fatalf("Authorization got error %v", err) + } + + if got, want := token.Password, "mytoken"; got != want { + t.Errorf("wanted token %q, got %q", want, got) + } +} + +// +// Keychain tests are in here so we can reuse the fake gcloud stuff. +// + +func mustRegistry(r string) name.Registry { + reg, err := name.NewRegistry(r, name.StrictValidation) + if err != nil { + panic(err) + } + return reg +} + +func TestKeychainDockerHub(t *testing.T) { + if auth, err := Keychain.Resolve(mustRegistry("index.docker.io")); err != nil { + t.Errorf("expected success, got: %v", err) + } else if auth != authn.Anonymous { + t.Errorf("expected anonymous, got: %v", auth) + } +} + +func TestKeychainGCRandAR(t *testing.T) { + cases := []struct { + host string + expectAuth bool + }{ + // GCR hosts + {"gcr.io", true}, + {"us.gcr.io", true}, + {"eu.gcr.io", true}, + {"asia.gcr.io", true}, + {"staging-k8s.gcr.io", true}, + {"global.gcr.io", true}, + {"notgcr.io", false}, + {"fake-gcr.io", false}, + {"alsonot.gcr.iot", false}, + // AR hosts + {"us-docker.pkg.dev", true}, + {"asia-docker.pkg.dev", true}, + {"europe-docker.pkg.dev", true}, + {"us-central1-docker.pkg.dev", true}, + {"us-docker-pkg.dev", false}, + {"someotherpkg.dev", false}, + {"looks-like-pkg.dev", false}, + {"closeto.pkg.devops", false}, + } + + // Env should fail. + if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil { + t.Fatalf("unexpected err os.Setenv: %v", err) + } + + for i, tc := range cases { + t.Run(fmt.Sprintf("cases[%d]", i), func(t *testing.T) { + // Reset the keychain to ensure we don't cache earlier results. + Keychain = &googleKeychain{} + + // Gcloud should succeed. + GetGcloudCmd = newGcloudCmdMock("success") + + if auth, err := Keychain.Resolve(mustRegistry(tc.host)); err != nil { + t.Errorf("expected success for %v, got: %v", tc.host, err) + } else if tc.expectAuth && auth == authn.Anonymous { + t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth) + } else if !tc.expectAuth && auth != authn.Anonymous { + t.Errorf("expected anonymous auth for %v, got: %v", tc, auth) + } + + // Make gcloud fail to test that caching works. + GetGcloudCmd = newGcloudCmdMock("badoutput") + + if auth, err := Keychain.Resolve(mustRegistry(tc.host)); err != nil { + t.Errorf("expected success for %v, got: %v", tc.host, err) + } else if tc.expectAuth && auth == authn.Anonymous { + t.Errorf("expected not anonymous auth for %v, got: %v", tc, auth) + } else if !tc.expectAuth && auth != authn.Anonymous { + t.Errorf("expected anonymous auth for %v, got: %v", tc, auth) + } + }) + } +} + +func TestKeychainError(t *testing.T) { + if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil { + t.Fatalf("unexpected err os.Setenv: %v", err) + } + + GetGcloudCmd = newGcloudCmdMock("badoutput") + + // Reset the keychain to ensure we don't cache earlier results. + Keychain = &googleKeychain{} + if auth, err := Keychain.Resolve(mustRegistry("gcr.io")); err != nil { + t.Fatalf("got error: %v", err) + } else if auth != authn.Anonymous { + t.Fatalf("wanted Anonymous, got %v", auth) + } +} + +type badSource struct{} + +func (bs badSource) Token() (*oauth2.Token, error) { + return nil, fmt.Errorf("oops") +} + +// This test is silly, but coverage. +func TestTokenSourceAuthError(t *testing.T) { + auth := tokenSourceAuth{badSource{}} + + _, err := auth.Authorization() + if err == nil { + t.Errorf("expected err, got nil") + } +} + +func TestNewEnvAuthenticatorFailure(t *testing.T) { + if err := os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "/dev/null"); err != nil { + t.Fatalf("unexpected err os.Setenv: %v", err) + } + + // Expect error. + _, err := NewEnvAuthenticator() + if err == nil { + t.Errorf("expected err, got nil") + } +} diff --git a/pkg/v1/google/doc.go b/pkg/v1/google/doc.go new file mode 100644 index 0000000..b6a67df --- /dev/null +++ b/pkg/v1/google/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package google provides facilities for listing images in gcr.io. +package google diff --git a/pkg/v1/google/keychain.go b/pkg/v1/google/keychain.go new file mode 100644 index 0000000..6dc7a50 --- /dev/null +++ b/pkg/v1/google/keychain.go @@ -0,0 +1,92 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "strings" + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" +) + +// Keychain exports an instance of the google Keychain. +var Keychain authn.Keychain = &googleKeychain{} + +type googleKeychain struct { + once sync.Once + auth authn.Authenticator +} + +// Resolve implements authn.Keychain a la docker-credential-gcr. +// +// This behaves similarly to the GCR credential helper, but reuses tokens until +// they expire. +// +// We can't easily add this behavior to our credential helper implementation +// of authn.Authenticator because the credential helper protocol doesn't include +// expiration information, see here: +// https://godoc.org/github.com/docker/docker-credential-helpers/credentials#Credentials +// +// In addition to being a performance optimization, the reuse of these access +// tokens works around a bug in gcloud. It appears that attempting to invoke +// `gcloud config config-helper` multiple times too quickly will fail: +// https://github.com/GoogleCloudPlatform/docker-credential-gcr/issues/54 +// +// We could upstream this behavior into docker-credential-gcr by parsing +// gcloud's output and persisting its tokens across invocations, but then +// we have to deal with invalidating caches across multiple runs (no fun). +// +// In general, we don't worry about that here because we expect to use the same +// gcloud configuration in the scope of this one process. +func (gk *googleKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { + // Only authenticate GCR and AR so it works with authn.NewMultiKeychain to fallback. + host := target.RegistryStr() + if host != "gcr.io" && + !strings.HasSuffix(host, ".gcr.io") && + !strings.HasSuffix(host, ".pkg.dev") && + !strings.HasSuffix(host, ".google.com") { + return authn.Anonymous, nil + } + + gk.once.Do(func() { + gk.auth = resolve() + }) + + return gk.auth, nil +} + +func resolve() authn.Authenticator { + auth, envErr := NewEnvAuthenticator() + if envErr == nil && auth != authn.Anonymous { + logs.Debug.Println("google.Keychain: using Application Default Credentials") + return auth + } + + auth, gErr := NewGcloudAuthenticator() + if gErr == nil && auth != authn.Anonymous { + logs.Debug.Println("google.Keychain: using gcloud fallback") + return auth + } + + logs.Debug.Println("Failed to get any Google credentials, falling back to Anonymous") + if envErr != nil { + logs.Debug.Printf("Google env error: %v", envErr) + } + if gErr != nil { + logs.Debug.Printf("gcloud error: %v", gErr) + } + return authn.Anonymous +} diff --git a/pkg/v1/google/list.go b/pkg/v1/google/list.go new file mode 100644 index 0000000..a70bb27 --- /dev/null +++ b/pkg/v1/google/list.go @@ -0,0 +1,331 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// Option is a functional option for List and Walk. +// TODO: Can we somehow reuse the remote options here? +type Option func(*lister) error + +type lister struct { + auth authn.Authenticator + transport http.RoundTripper + repo name.Repository + client *http.Client + ctx context.Context + userAgent string +} + +func newLister(repo name.Repository, options ...Option) (*lister, error) { + l := &lister{ + auth: authn.Anonymous, + transport: http.DefaultTransport, + repo: repo, + ctx: context.Background(), + } + + for _, option := range options { + if err := option(l); err != nil { + return nil, err + } + } + + // transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping. + // This is to allow consumers full control over the transports logic, such as providing retry logic. + if _, ok := l.transport.(*transport.Wrapper); !ok { + // Wrap the transport in something that logs requests and responses. + // It's expensive to generate the dumps, so skip it if we're writing + // to nothing. + if logs.Enabled(logs.Debug) { + l.transport = transport.NewLogger(l.transport) + } + + // Wrap the transport in something that can retry network flakes. + l.transport = transport.NewRetry(l.transport) + + // Wrap this last to prevent transport.New from double-wrapping. + if l.userAgent != "" { + l.transport = transport.NewUserAgent(l.transport, l.userAgent) + } + } + + scopes := []string{repo.Scope(transport.PullScope)} + tr, err := transport.NewWithContext(l.ctx, repo.Registry, l.auth, l.transport, scopes) + if err != nil { + return nil, err + } + + l.client = &http.Client{Transport: tr} + + return l, nil +} + +func (l *lister) list(repo name.Repository) (*Tags, error) { + uri := &url.URL{ + Scheme: repo.Registry.Scheme(), + Host: repo.Registry.RegistryStr(), + Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()), + // ECR returns an error if n > 1000: + // https://github.com/google/go-containerregistry/issues/681 + RawQuery: "n=1000", + } + + tags := Tags{} + + // get responses until there is no next page + for { + select { + case <-l.ctx.Done(): + return nil, l.ctx.Err() + default: + } + + req, err := http.NewRequest("GET", uri.String(), nil) + if err != nil { + return nil, err + } + req = req.WithContext(l.ctx) + + resp, err := l.client.Do(req) + if err != nil { + return nil, err + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + parsed := Tags{} + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return nil, err + } + + if err := resp.Body.Close(); err != nil { + return nil, err + } + + if len(parsed.Manifests) != 0 || len(parsed.Children) != 0 { + // We're dealing with GCR, just return directly. + return &parsed, nil + } + + // This isn't GCR, just append the tags and keep paginating. + tags.Tags = append(tags.Tags, parsed.Tags...) + + uri, err = getNextPageURL(resp) + if err != nil { + return nil, err + } + // no next page + if uri == nil { + break + } + logs.Warn.Printf("saw non-google tag listing response, falling back to pagination") + } + + return &tags, nil +} + +// getNextPageURL checks if there is a Link header in a http.Response which +// contains a link to the next page. If yes it returns the url.URL of the next +// page otherwise it returns nil. +func getNextPageURL(resp *http.Response) (*url.URL, error) { + link := resp.Header.Get("Link") + if link == "" { + return nil, nil + } + + if link[0] != '<' { + return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link) + } + + end := strings.Index(link, ">") + if end == -1 { + return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link) + } + link = link[1:end] + + linkURL, err := url.Parse(link) + if err != nil { + return nil, err + } + if resp.Request == nil || resp.Request.URL == nil { + return nil, nil + } + linkURL = resp.Request.URL.ResolveReference(linkURL) + return linkURL, nil +} + +type rawManifestInfo struct { + Size string `json:"imageSizeBytes"` + MediaType string `json:"mediaType"` + Created string `json:"timeCreatedMs"` + Uploaded string `json:"timeUploadedMs"` + Tags []string `json:"tag"` +} + +// ManifestInfo is a Manifests entry is the output of List and Walk. +type ManifestInfo struct { + Size uint64 `json:"imageSizeBytes"` + MediaType string `json:"mediaType"` + Created time.Time `json:"timeCreatedMs"` + Uploaded time.Time `json:"timeUploadedMs"` + Tags []string `json:"tag"` +} + +func fromUnixMs(ms int64) time.Time { + sec := ms / 1000 + ns := (ms % 1000) * 1000000 + return time.Unix(sec, ns) +} + +func toUnixMs(t time.Time) string { + return strconv.FormatInt(t.UnixNano()/1000000, 10) +} + +// MarshalJSON implements json.Marshaler +func (m ManifestInfo) MarshalJSON() ([]byte, error) { + return json.Marshal(rawManifestInfo{ + Size: strconv.FormatUint(m.Size, 10), + MediaType: m.MediaType, + Created: toUnixMs(m.Created), + Uploaded: toUnixMs(m.Uploaded), + Tags: m.Tags, + }) +} + +// UnmarshalJSON implements json.Unmarshaler +func (m *ManifestInfo) UnmarshalJSON(data []byte) error { + raw := rawManifestInfo{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + if raw.Size != "" { + size, err := strconv.ParseUint(raw.Size, 10, 64) + if err != nil { + return err + } + m.Size = size + } + + if raw.Created != "" { + created, err := strconv.ParseInt(raw.Created, 10, 64) + if err != nil { + return err + } + m.Created = fromUnixMs(created) + } + + if raw.Uploaded != "" { + uploaded, err := strconv.ParseInt(raw.Uploaded, 10, 64) + if err != nil { + return err + } + m.Uploaded = fromUnixMs(uploaded) + } + + m.MediaType = raw.MediaType + m.Tags = raw.Tags + + return nil +} + +// Tags is the result of List and Walk. +type Tags struct { + Children []string `json:"child"` + Manifests map[string]ManifestInfo `json:"manifest"` + Name string `json:"name"` + Tags []string `json:"tags"` +} + +// List calls /tags/list for the given repository. +func List(repo name.Repository, options ...Option) (*Tags, error) { + l, err := newLister(repo, options...) + if err != nil { + return nil, err + } + + return l.list(repo) +} + +// WalkFunc is the type of the function called for each repository visited by +// Walk. This implements a similar API to filepath.Walk. +// +// The repo argument contains the argument to Walk as a prefix; that is, if Walk +// is called with "gcr.io/foo", which is a repository containing the repository +// "bar", the walk function will be called with argument "gcr.io/foo/bar". +// The tags and error arguments are the result of calling List on repo. +// +// TODO: Do we want a SkipDir error, as in filepath.WalkFunc? +type WalkFunc func(repo name.Repository, tags *Tags, err error) error + +func walk(repo name.Repository, tags *Tags, walkFn WalkFunc, options ...Option) error { + if tags == nil { + // This shouldn't happen. + return fmt.Errorf("tags nil for %q", repo) + } + + if err := walkFn(repo, tags, nil); err != nil { + return err + } + + for _, path := range tags.Children { + child, err := name.NewRepository(fmt.Sprintf("%s/%s", repo, path), name.StrictValidation) + if err != nil { + // We don't expect this ever, so don't pass it through to walkFn. + return fmt.Errorf("unexpected path failure: %w", err) + } + + childTags, err := List(child, options...) + if err != nil { + if err := walkFn(child, nil, err); err != nil { + return err + } + } else { + if err := walk(child, childTags, walkFn, options...); err != nil { + return err + } + } + } + + // We made it! + return nil +} + +// Walk recursively descends repositories, calling walkFn. +func Walk(root name.Repository, walkFn WalkFunc, options ...Option) error { + tags, err := List(root, options...) + if err != nil { + return walkFn(root, nil, err) + } + + return walk(root, tags, walkFn, options...) +} diff --git a/pkg/v1/google/list_test.go b/pkg/v1/google/list_test.go new file mode 100644 index 0000000..5718526 --- /dev/null +++ b/pkg/v1/google/list_test.go @@ -0,0 +1,339 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" +) + +func mustParseDuration(t *testing.T, d string) time.Duration { + dur, err := time.ParseDuration(d) + if err != nil { + t.Fatal(err) + } + return dur +} + +func TestRoundtrip(t *testing.T) { + raw := rawManifestInfo{ + Size: "100", + MediaType: "hi", + Created: "12345678", + Uploaded: "23456789", + Tags: []string{"latest"}, + } + + og, err := json.Marshal(raw) + if err != nil { + t.Fatal(err) + } + + parsed := ManifestInfo{} + if err := json.Unmarshal(og, &parsed); err != nil { + t.Fatal(err) + } + + roundtripped, err := json.Marshal(parsed) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(og, roundtripped); diff != "" { + t.Errorf("ManifestInfo can't roundtrip: (-want +got) = %s", diff) + } +} + +func TestList(t *testing.T) { + cases := []struct { + name string + responseBody []byte + wantErr bool + wantTags *Tags + }{{ + name: "success", + responseBody: []byte(`{"tags":["foo","bar"]}`), + wantErr: false, + wantTags: &Tags{Tags: []string{"foo", "bar"}}, + }, { + name: "gcr success", + responseBody: []byte(`{"child":["hello", "world"],"manifest":{"digest1":{"imageSizeBytes":"1","mediaType":"mainstream","timeCreatedms":"1","timeUploadedMs":"2","tag":["foo"]},"digest2":{"imageSizeBytes":"2","mediaType":"indie","timeCreatedMs":"3","timeUploadedMs":"4","tag":["bar","baz"]}},"tags":["foo","bar","baz"]}`), + wantErr: false, + wantTags: &Tags{ + Children: []string{"hello", "world"}, + Manifests: map[string]ManifestInfo{ + "digest1": { + Size: 1, + MediaType: "mainstream", + Created: time.Unix(0, 0).Add(mustParseDuration(t, "1ms")), + Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "2ms")), + Tags: []string{"foo"}, + }, + "digest2": { + Size: 2, + MediaType: "indie", + Created: time.Unix(0, 0).Add(mustParseDuration(t, "3ms")), + Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "4ms")), + Tags: []string{"bar", "baz"}, + }, + }, + Tags: []string{"foo", "bar", "baz"}, + }, + }, { + name: "just children", + responseBody: []byte(`{"child":["hello", "world"]}`), + wantErr: false, + wantTags: &Tags{ + Children: []string{"hello", "world"}, + }, + }, { + name: "not json", + responseBody: []byte("notjson"), + wantErr: true, + }} + + repoName := "ubuntu" + // To test WithUserAgent + uaSentinel := "this-is-the-user-agent" + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tagsPath := fmt.Sprintf("/v2/%s/tags/list", repoName) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("User-Agent"), uaSentinel; !strings.Contains(got, want) { + t.Errorf("request did not container useragent, got %q want Contains(%q)", got, want) + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case tagsPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Write(tc.responseBody) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation) + if err != nil { + t.Fatalf("name.NewRepository(%v) = %v", repoName, err) + } + + tags, err := List(repo, WithAuthFromKeychain(authn.DefaultKeychain), WithTransport(http.DefaultTransport), WithUserAgent(uaSentinel), WithContext(context.Background())) + if (err != nil) != tc.wantErr { + t.Errorf("List() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + + if diff := cmp.Diff(tc.wantTags, tags); diff != "" { + t.Errorf("List() wrong tags (-want +got) = %s", diff) + } + }) + } +} + +type recorder struct { + Tags []*Tags + Errs []error +} + +func (r *recorder) walk(repo name.Repository, tags *Tags, err error) error { + r.Tags = append(r.Tags, tags) + r.Errs = append(r.Errs, err) + + return nil +} + +func TestWalk(t *testing.T) { + // Stupid coverage to make sure it doesn't panic. + var b bytes.Buffer + logs.Debug.SetOutput(&b) + + cases := []struct { + name string + responseBody []byte + wantResult recorder + }{{ + name: "gcr success", + responseBody: []byte(`{"child":["hello", "world"],"manifest":{"digest1":{"imageSizeBytes":"1","mediaType":"mainstream","timeCreatedms":"1","timeUploadedMs":"2","tag":["foo"]},"digest2":{"imageSizeBytes":"2","mediaType":"indie","timeCreatedMs":"3","timeUploadedMs":"4","tag":["bar","baz"]}},"tags":["foo","bar","baz"]}`), + wantResult: recorder{ + Tags: []*Tags{{ + Children: []string{"hello", "world"}, + Manifests: map[string]ManifestInfo{ + "digest1": { + Size: 1, + MediaType: "mainstream", + Created: time.Unix(0, 0).Add(mustParseDuration(t, "1ms")), + Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "2ms")), + Tags: []string{"foo"}, + }, + "digest2": { + Size: 2, + MediaType: "indie", + Created: time.Unix(0, 0).Add(mustParseDuration(t, "3ms")), + Uploaded: time.Unix(0, 0).Add(mustParseDuration(t, "4ms")), + Tags: []string{"bar", "baz"}, + }, + }, + Tags: []string{"foo", "bar", "baz"}, + }, { + Tags: []string{"hello"}, + }, { + Tags: []string{"world"}, + }}, + Errs: []error{nil, nil, nil}, + }, + }} + + repoName := "ubuntu" + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + rootPath := fmt.Sprintf("/v2/%s/tags/list", repoName) + helloPath := fmt.Sprintf("/v2/%s/hello/tags/list", repoName) + worldPath := fmt.Sprintf("/v2/%s/world/tags/list", repoName) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case rootPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Write(tc.responseBody) + case helloPath: + w.Write([]byte(`{"tags":["hello"]}`)) + case worldPath: + w.Write([]byte(`{"tags":["world"]}`)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation) + if err != nil { + t.Fatalf("name.NewRepository(%v) = %v", repoName, err) + } + + r := recorder{} + if err := Walk(repo, r.walk, WithAuth(authn.Anonymous)); err != nil { + t.Errorf("unexpected err: %v", err) + } + + if diff := cmp.Diff(tc.wantResult, r); diff != "" { + t.Errorf("Walk() wrong tags (-want +got) = %s", diff) + } + }) + } +} + +// Copied shamelessly from remote. +func TestCancelledList(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + repoName := "doesnotmatter" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation) + if err != nil { + t.Fatalf("name.NewRepository(%v) = %v", repoName, err) + } + + _, err = List(repo, WithContext(ctx)) + if !strings.Contains(err.Error(), context.Canceled.Error()) { + t.Errorf("wanted %q to contain %q", err.Error(), context.Canceled.Error()) + } +} + +func makeResp(hdr string) *http.Response { + return &http.Response{ + Header: http.Header{ + "Link": []string{hdr}, + }, + } +} + +func TestGetNextPageURL(t *testing.T) { + for _, hdr := range []string{ + "", + "<", + "><", + "<>", + fmt.Sprintf("<%c>", 0x7f), // makes url.Parse fail + } { + u, err := getNextPageURL(makeResp(hdr)) + if err == nil && u != nil { + t.Errorf("Expected err, got %+v", u) + } + } + + good := &http.Response{ + Header: http.Header{ + "Link": []string{"<example.com>"}, + }, + Request: &http.Request{ + URL: &url.URL{ + Scheme: "https", + }, + }, + } + u, err := getNextPageURL(good) + if err != nil { + t.Fatal(err) + } + + if u.Scheme != "https" { + t.Errorf("expected scheme to match request, got %s", u.Scheme) + } +} diff --git a/pkg/v1/google/options.go b/pkg/v1/google/options.go new file mode 100644 index 0000000..604808c --- /dev/null +++ b/pkg/v1/google/options.go @@ -0,0 +1,73 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package google + +import ( + "context" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" +) + +// WithTransport is a functional option for overriding the default transport +// on a remote image +func WithTransport(t http.RoundTripper) Option { + return func(l *lister) error { + l.transport = t + return nil + } +} + +// WithAuth is a functional option for overriding the default authenticator +// on a remote image +func WithAuth(auth authn.Authenticator) Option { + return func(l *lister) error { + l.auth = auth + return nil + } +} + +// WithAuthFromKeychain is a functional option for overriding the default +// authenticator on a remote image using an authn.Keychain +func WithAuthFromKeychain(keys authn.Keychain) Option { + return func(l *lister) error { + auth, err := keys.Resolve(l.repo.Registry) + if err != nil { + return err + } + l.auth = auth + return nil + } +} + +// WithContext is a functional option for overriding the default +// context.Context for HTTP request to list remote images +func WithContext(ctx context.Context) Option { + return func(l *lister) error { + l.ctx = ctx + return nil + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. This header will also include "go-containerregistry/${version}". +// +// If you want to completely overwrite the User-Agent header, use WithTransport. +func WithUserAgent(ua string) Option { + return func(l *lister) error { + l.userAgent = ua + return nil + } +} diff --git a/pkg/v1/google/testdata/README.md b/pkg/v1/google/testdata/README.md new file mode 100644 index 0000000..12222aa --- /dev/null +++ b/pkg/v1/google/testdata/README.md @@ -0,0 +1,4 @@ +# testdata + +This key is cribbed from [here](https://github.com/golang/oauth2/blob/d668ce993890a79bda886613ee587a69dd5da7a6/google/testdata/gcloud/credentials). +It's invalid but parses sufficiently to test `NewEnvAuthenticator`. diff --git a/pkg/v1/google/testdata/key.json b/pkg/v1/google/testdata/key.json new file mode 100644 index 0000000..c2d23ce --- /dev/null +++ b/pkg/v1/google/testdata/key.json @@ -0,0 +1,35 @@ +{ + "_class": "OAuth2Credentials", + "_module": "oauth2client.client", + "access_token": "foo_access_token", + "client_id": "foo_client_id", + "client_secret": "foo_client_secret", + "id_token": { + "at_hash": "foo_at_hash", + "aud": "foo_aud", + "azp": "foo_azp", + "cid": "foo_cid", + "email": "foo@example.com", + "email_verified": true, + "exp": 1420573614, + "iat": 1420569714, + "id": "1337", + "iss": "accounts.google.com", + "sub": "1337", + "token_hash": "foo_token_hash", + "verified_email": true + }, + "invalid": false, + "refresh_token": "foo_refresh_token", + "revoke_uri": "https://accounts.google.com/o/oauth2/revoke", + "token_expiry": "3015-01-09T00:51:51Z", + "token_response": { + "access_token": "foo_access_token", + "expires_in": 3600, + "id_token": "foo_id_token", + "token_type": "Bearer" + }, + "token_uri": "https://accounts.google.com/o/oauth2/token", + "user_agent": "Cloud SDK Command Line Tool", + "type": "authorized_user" +} diff --git a/pkg/v1/hash.go b/pkg/v1/hash.go new file mode 100644 index 0000000..f78a5fa --- /dev/null +++ b/pkg/v1/hash.go @@ -0,0 +1,123 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "crypto" + "encoding/hex" + "encoding/json" + "fmt" + "hash" + "io" + "strconv" + "strings" +) + +// Hash is an unqualified digest of some content, e.g. sha256:deadbeef +type Hash struct { + // Algorithm holds the algorithm used to compute the hash. + Algorithm string + + // Hex holds the hex portion of the content hash. + Hex string +} + +// String reverses NewHash returning the string-form of the hash. +func (h Hash) String() string { + return fmt.Sprintf("%s:%s", h.Algorithm, h.Hex) +} + +// NewHash validates the input string is a hash and returns a strongly type Hash object. +func NewHash(s string) (Hash, error) { + h := Hash{} + if err := h.parse(s); err != nil { + return Hash{}, err + } + return h, nil +} + +// MarshalJSON implements json.Marshaler +func (h Hash) MarshalJSON() ([]byte, error) { + return json.Marshal(h.String()) +} + +// UnmarshalJSON implements json.Unmarshaler +func (h *Hash) UnmarshalJSON(data []byte) error { + s, err := strconv.Unquote(string(data)) + if err != nil { + return err + } + return h.parse(s) +} + +// MarshalText implements encoding.TextMarshaler. This is required to use +// v1.Hash as a key in a map when marshalling JSON. +func (h Hash) MarshalText() (text []byte, err error) { + return []byte(h.String()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. This is required to use +// v1.Hash as a key in a map when unmarshalling JSON. +func (h *Hash) UnmarshalText(text []byte) error { + return h.parse(string(text)) +} + +// Hasher returns a hash.Hash for the named algorithm (e.g. "sha256") +func Hasher(name string) (hash.Hash, error) { + switch name { + case "sha256": + return crypto.SHA256.New(), nil + default: + return nil, fmt.Errorf("unsupported hash: %q", name) + } +} + +func (h *Hash) parse(unquoted string) error { + parts := strings.Split(unquoted, ":") + if len(parts) != 2 { + return fmt.Errorf("cannot parse hash: %q", unquoted) + } + + rest := strings.TrimLeft(parts[1], "0123456789abcdef") + if len(rest) != 0 { + return fmt.Errorf("found non-hex character in hash: %c", rest[0]) + } + + hasher, err := Hasher(parts[0]) + if err != nil { + return err + } + // Compare the hex to the expected size (2 hex characters per byte) + if len(parts[1]) != hasher.Size()*2 { + return fmt.Errorf("wrong number of hex digits for %s: %s", parts[0], parts[1]) + } + + h.Algorithm = parts[0] + h.Hex = parts[1] + return nil +} + +// SHA256 computes the Hash of the provided io.Reader's content. +func SHA256(r io.Reader) (Hash, int64, error) { + hasher := crypto.SHA256.New() + n, err := io.Copy(hasher, r) + if err != nil { + return Hash{}, 0, err + } + return Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), + }, n, nil +} diff --git a/pkg/v1/hash_test.go b/pkg/v1/hash_test.go new file mode 100644 index 0000000..df1be77 --- /dev/null +++ b/pkg/v1/hash_test.go @@ -0,0 +1,115 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "strconv" + "strings" + "testing" +) + +func TestGoodHashes(t *testing.T) { + good := []string{ + "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + } + + for _, s := range good { + h, err := NewHash(s) + if err != nil { + t.Error("Unexpected error parsing hash:", err) + } + if got, want := h.String(), s; got != want { + t.Errorf("String(); got %q, want %q", got, want) + } + bytes, err := json.Marshal(h) + if err != nil { + t.Error("Unexpected error json.Marshaling hash:", err) + } + if got, want := string(bytes), strconv.Quote(h.String()); got != want { + t.Errorf("json.Marshal(); got %q, want %q", got, want) + } + } +} + +func TestBadHashes(t *testing.T) { + bad := []string{ + // Too short + "sha256:deadbeef", + // Bad character + "sha256:o123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + // Unknown algorithm + "md5:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + // Too few parts + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + // Too many parts + "md5:sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + } + + for _, s := range bad { + h, err := NewHash(s) + if err == nil { + t.Error("Expected error, got:", h) + } + } +} + +func TestSHA256(t *testing.T) { + input := "asdf" + h, n, err := SHA256(strings.NewReader(input)) + if err != nil { + t.Error("SHA256(asdf) =", err) + } + if got, want := h.Algorithm, "sha256"; got != want { + t.Errorf("Algorithm; got %v, want %v", got, want) + } + if got, want := h.Hex, "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"; got != want { + t.Errorf("Hex; got %v, want %v", got, want) + } + if got, want := n, int64(len(input)); got != want { + t.Errorf("n; got %v, want %v", got, want) + } +} + +// This tests that you can use Hash as a key in a map (needs to implement both +// MarshalText and UnmarshalText). +func TestTextMarshalling(t *testing.T) { + foo := make(map[Hash]string) + b, err := json.Marshal(foo) + if err != nil { + t.Fatal("could not marshal:", err) + } + if err := json.Unmarshal(b, &foo); err != nil { + t.Error("could not unmarshal:", err) + } + + h := &Hash{ + Algorithm: "sha256", + Hex: strings.Repeat("a", 64), + } + g := &Hash{} + text, err := h.MarshalText() + if err != nil { + t.Fatal(err) + } + if err := g.UnmarshalText(text); err != nil { + t.Fatal(err) + } + + if h.String() != g.String() { + t.Errorf("mismatched hash: %s != %s", h, g) + } +} diff --git a/pkg/v1/image.go b/pkg/v1/image.go new file mode 100644 index 0000000..8de9e47 --- /dev/null +++ b/pkg/v1/image.go @@ -0,0 +1,59 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Image defines the interface for interacting with an OCI v1 image. +type Image interface { + // Layers returns the ordered collection of filesystem layers that comprise this image. + // The order of the list is oldest/base layer first, and most-recent/top layer last. + Layers() ([]Layer, error) + + // MediaType of this image's manifest. + MediaType() (types.MediaType, error) + + // Size returns the size of the manifest. + Size() (int64, error) + + // ConfigName returns the hash of the image's config file, also known as + // the Image ID. + ConfigName() (Hash, error) + + // ConfigFile returns this image's config file. + ConfigFile() (*ConfigFile, error) + + // RawConfigFile returns the serialized bytes of ConfigFile(). + RawConfigFile() ([]byte, error) + + // Digest returns the sha256 of this image's manifest. + Digest() (Hash, error) + + // Manifest returns this image's Manifest object. + Manifest() (*Manifest, error) + + // RawManifest returns the serialized bytes of Manifest() + RawManifest() ([]byte, error) + + // LayerByDigest returns a Layer for interacting with a particular layer of + // the image, looking it up by "digest" (the compressed hash). + LayerByDigest(Hash) (Layer, error) + + // LayerByDiffID is an analog to LayerByDigest, looking up by "diff id" + // (the uncompressed hash). + LayerByDiffID(Hash) (Layer, error) +} diff --git a/pkg/v1/index.go b/pkg/v1/index.go new file mode 100644 index 0000000..8e7bc8e --- /dev/null +++ b/pkg/v1/index.go @@ -0,0 +1,43 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// ImageIndex defines the interface for interacting with an OCI image index. +type ImageIndex interface { + // MediaType of this image's manifest. + MediaType() (types.MediaType, error) + + // Digest returns the sha256 of this index's manifest. + Digest() (Hash, error) + + // Size returns the size of the manifest. + Size() (int64, error) + + // IndexManifest returns this image index's manifest object. + IndexManifest() (*IndexManifest, error) + + // RawManifest returns the serialized bytes of IndexManifest(). + RawManifest() ([]byte, error) + + // Image returns a v1.Image that this ImageIndex references. + Image(Hash) (Image, error) + + // ImageIndex returns a v1.ImageIndex that this ImageIndex references. + ImageIndex(Hash) (ImageIndex, error) +} diff --git a/pkg/v1/layer.go b/pkg/v1/layer.go new file mode 100644 index 0000000..57447d2 --- /dev/null +++ b/pkg/v1/layer.go @@ -0,0 +1,42 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "io" + + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Layer is an interface for accessing the properties of a particular layer of a v1.Image +type Layer interface { + // Digest returns the Hash of the compressed layer. + Digest() (Hash, error) + + // DiffID returns the Hash of the uncompressed layer. + DiffID() (Hash, error) + + // Compressed returns an io.ReadCloser for the compressed layer contents. + Compressed() (io.ReadCloser, error) + + // Uncompressed returns an io.ReadCloser for the uncompressed layer contents. + Uncompressed() (io.ReadCloser, error) + + // Size returns the compressed size of the Layer. + Size() (int64, error) + + // MediaType returns the media type of the Layer. + MediaType() (types.MediaType, error) +} diff --git a/pkg/v1/layout/README.md b/pkg/v1/layout/README.md new file mode 100644 index 0000000..54bee6d --- /dev/null +++ b/pkg/v1/layout/README.md @@ -0,0 +1,5 @@ +# `layout` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/layout) + +The `layout` package implements support for interacting with an [OCI Image Layout](https://github.com/opencontainers/image-spec/blob/master/image-layout.md). diff --git a/pkg/v1/layout/blob.go b/pkg/v1/layout/blob.go new file mode 100644 index 0000000..2e5f435 --- /dev/null +++ b/pkg/v1/layout/blob.go @@ -0,0 +1,37 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "io" + "os" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// Blob returns a blob with the given hash from the Path. +func (l Path) Blob(h v1.Hash) (io.ReadCloser, error) { + return os.Open(l.blobPath(h)) +} + +// Bytes is a convenience function to return a blob from the Path as +// a byte slice. +func (l Path) Bytes(h v1.Hash) ([]byte, error) { + return os.ReadFile(l.blobPath(h)) +} + +func (l Path) blobPath(h v1.Hash) string { + return l.path("blobs", h.Algorithm, h.Hex) +} diff --git a/pkg/v1/layout/doc.go b/pkg/v1/layout/doc.go new file mode 100644 index 0000000..d80d273 --- /dev/null +++ b/pkg/v1/layout/doc.go @@ -0,0 +1,19 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package layout provides facilities for reading/writing artifacts from/to +// an OCI image layout on disk, see: +// +// https://github.com/opencontainers/image-spec/blob/master/image-layout.md +package layout diff --git a/pkg/v1/layout/image.go b/pkg/v1/layout/image.go new file mode 100644 index 0000000..c9ae966 --- /dev/null +++ b/pkg/v1/layout/image.go @@ -0,0 +1,139 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "fmt" + "io" + "os" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type layoutImage struct { + path Path + desc v1.Descriptor + manifestLock sync.Mutex // Protects rawManifest + rawManifest []byte +} + +var _ partial.CompressedImageCore = (*layoutImage)(nil) + +// Image reads a v1.Image with digest h from the Path. +func (l Path) Image(h v1.Hash) (v1.Image, error) { + ii, err := l.ImageIndex() + if err != nil { + return nil, err + } + + return ii.Image(h) +} + +func (li *layoutImage) MediaType() (types.MediaType, error) { + return li.desc.MediaType, nil +} + +// Implements WithManifest for partial.Blobset. +func (li *layoutImage) Manifest() (*v1.Manifest, error) { + return partial.Manifest(li) +} + +func (li *layoutImage) RawManifest() ([]byte, error) { + li.manifestLock.Lock() + defer li.manifestLock.Unlock() + if li.rawManifest != nil { + return li.rawManifest, nil + } + + b, err := li.path.Bytes(li.desc.Digest) + if err != nil { + return nil, err + } + + li.rawManifest = b + return li.rawManifest, nil +} + +func (li *layoutImage) RawConfigFile() ([]byte, error) { + manifest, err := li.Manifest() + if err != nil { + return nil, err + } + + return li.path.Bytes(manifest.Config.Digest) +} + +func (li *layoutImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) { + manifest, err := li.Manifest() + if err != nil { + return nil, err + } + + if h == manifest.Config.Digest { + return &compressedBlob{ + path: li.path, + desc: manifest.Config, + }, nil + } + + for _, desc := range manifest.Layers { + if h == desc.Digest { + return &compressedBlob{ + path: li.path, + desc: desc, + }, nil + } + } + + return nil, fmt.Errorf("could not find layer in image: %s", h) +} + +type compressedBlob struct { + path Path + desc v1.Descriptor +} + +func (b *compressedBlob) Digest() (v1.Hash, error) { + return b.desc.Digest, nil +} + +func (b *compressedBlob) Compressed() (io.ReadCloser, error) { + return b.path.Blob(b.desc.Digest) +} + +func (b *compressedBlob) Size() (int64, error) { + return b.desc.Size, nil +} + +func (b *compressedBlob) MediaType() (types.MediaType, error) { + return b.desc.MediaType, nil +} + +// Descriptor implements partial.withDescriptor. +func (b *compressedBlob) Descriptor() (*v1.Descriptor, error) { + return &b.desc, nil +} + +// See partial.Exists. +func (b *compressedBlob) Exists() (bool, error) { + _, err := os.Stat(b.path.blobPath(b.desc.Digest)) + if os.IsNotExist(err) { + return false, nil + } + return err == nil, err +} diff --git a/pkg/v1/layout/image_test.go b/pkg/v1/layout/image_test.go new file mode 100644 index 0000000..3614920 --- /dev/null +++ b/pkg/v1/layout/image_test.go @@ -0,0 +1,181 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "path/filepath" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +var ( + indexDigest = v1.Hash{ + Algorithm: "sha256", + Hex: "05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5", + } + manifestDigest = v1.Hash{ + Algorithm: "sha256", + Hex: "eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650", + } + configDigest = v1.Hash{ + Algorithm: "sha256", + Hex: "6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e", + } + bogusDigest = v1.Hash{ + Algorithm: "sha256", + Hex: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + } + customManifestDigest = v1.Hash{ + Algorithm: "sha256", + Hex: "b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e", + } + bogusPath = filepath.Join("testdata", "does_not_exist") + testPath = filepath.Join("testdata", "test_index") + testPathOneImage = filepath.Join("testdata", "test_index_one_image") + testPathMediaType = filepath.Join("testdata", "test_index_media_type") + customMediaType types.MediaType = "application/tar+gzip" +) + +func TestImage(t *testing.T) { + lp, err := FromPath(testPath) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + img, err := lp.Image(manifestDigest) + if err != nil { + t.Fatalf("Image() = %v", err) + } + + if err := validate.Image(img); err != nil { + t.Errorf("validate.Image() = %v", err) + } + + mt, err := img.MediaType() + if err != nil { + t.Errorf("MediaType() = %v", err) + } else if got, want := mt, types.OCIManifestSchema1; got != want { + t.Errorf("MediaType(); want: %v got: %v", want, got) + } + + cfg, err := img.LayerByDigest(configDigest) + if err != nil { + t.Fatalf("LayerByDigest(%s) = %v", configDigest, err) + } + + cfgName, err := img.ConfigName() + if err != nil { + t.Fatalf("ConfigName() = %v", err) + } + + cfgDigest, err := cfg.Digest() + if err != nil { + t.Fatalf("cfg.Digest() = %v", err) + } + + if got, want := cfgDigest, cfgName; got != want { + t.Errorf("ConfigName(); want: %v got: %v", want, got) + } + + layers, err := img.Layers() + if err != nil { + t.Fatalf("img.Layers() = %v", err) + } + + mediaType, err := layers[0].MediaType() + if err != nil { + t.Fatalf("img.Layers() = %v", err) + } + + // Fixture is a DockerLayer + if got, want := mediaType, types.DockerLayer; got != want { + t.Fatalf("MediaType(); want: %q got: %q", want, got) + } + + if ok, err := partial.Exists(layers[0]); err != nil { + t.Fatal(err) + } else if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } +} + +func TestImageWithEmptyHash(t *testing.T) { + lp, err := FromPath(testPathOneImage) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + img, err := lp.Image(v1.Hash{}) + if err != nil { + t.Fatalf("Image() = %v", err) + } + + if err := validate.Image(img); err != nil { + t.Errorf("validate.Image() = %v", err) + } +} + +func TestImageErrors(t *testing.T) { + lp, err := FromPath(testPath) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + img, err := lp.Image(manifestDigest) + if err != nil { + t.Fatalf("Image() = %v", err) + } + + if _, err := img.LayerByDigest(bogusDigest); err == nil { + t.Errorf("LayerByDigest(%s) = nil, expected err", bogusDigest) + } + + if _, err := lp.Image(bogusDigest); err == nil { + t.Errorf("Image(%s) = nil, expected err", bogusDigest) + } + + if _, err := lp.Image(bogusDigest); err == nil { + t.Errorf("Image(%s, %s) = nil, expected err", bogusPath, bogusDigest) + } +} + +func TestImageCustomMediaType(t *testing.T) { + lp, err := FromPath(testPathMediaType) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + img, err := lp.Image(customManifestDigest) + if err != nil { + t.Fatalf("Image() = %v", err) + } + mt, err := img.MediaType() + if err != nil { + t.Errorf("MediaType() = %v", err) + } else if got, want := mt, types.OCIManifestSchema1; got != want { + t.Errorf("MediaType(); want: %v got: %v", want, got) + } + layers, err := img.Layers() + if err != nil { + t.Fatalf("img.Layers() = %v", err) + } + mediaType, err := layers[0].MediaType() + if err != nil { + t.Fatalf("img.Layers() = %v", err) + } + if got, want := mediaType, customMediaType; got != want { + t.Fatalf("MediaType(); want: %q got: %q", want, got) + } +} diff --git a/pkg/v1/layout/index.go b/pkg/v1/layout/index.go new file mode 100644 index 0000000..7404f18 --- /dev/null +++ b/pkg/v1/layout/index.go @@ -0,0 +1,161 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var _ v1.ImageIndex = (*layoutIndex)(nil) + +type layoutIndex struct { + mediaType types.MediaType + path Path + rawIndex []byte +} + +// ImageIndexFromPath is a convenience function which constructs a Path and returns its v1.ImageIndex. +func ImageIndexFromPath(path string) (v1.ImageIndex, error) { + lp, err := FromPath(path) + if err != nil { + return nil, err + } + return lp.ImageIndex() +} + +// ImageIndex returns a v1.ImageIndex for the Path. +func (l Path) ImageIndex() (v1.ImageIndex, error) { + rawIndex, err := os.ReadFile(l.path("index.json")) + if err != nil { + return nil, err + } + + idx := &layoutIndex{ + mediaType: types.OCIImageIndex, + path: l, + rawIndex: rawIndex, + } + + return idx, nil +} + +func (i *layoutIndex) MediaType() (types.MediaType, error) { + return i.mediaType, nil +} + +func (i *layoutIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i *layoutIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i *layoutIndex) IndexManifest() (*v1.IndexManifest, error) { + var index v1.IndexManifest + err := json.Unmarshal(i.rawIndex, &index) + return &index, err +} + +func (i *layoutIndex) RawManifest() ([]byte, error) { + return i.rawIndex, nil +} + +func (i *layoutIndex) Image(h v1.Hash) (v1.Image, error) { + // Look up the digest in our manifest first to return a better error. + desc, err := i.findDescriptor(h) + if err != nil { + return nil, err + } + + if !isExpectedMediaType(desc.MediaType, types.OCIManifestSchema1, types.DockerManifestSchema2) { + return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType) + } + + img := &layoutImage{ + path: i.path, + desc: *desc, + } + return partial.CompressedToImage(img) +} + +func (i *layoutIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + // Look up the digest in our manifest first to return a better error. + desc, err := i.findDescriptor(h) + if err != nil { + return nil, err + } + + if !isExpectedMediaType(desc.MediaType, types.OCIImageIndex, types.DockerManifestList) { + return nil, fmt.Errorf("unexpected media type for %v: %s", h, desc.MediaType) + } + + rawIndex, err := i.path.Bytes(h) + if err != nil { + return nil, err + } + + return &layoutIndex{ + mediaType: desc.MediaType, + path: i.path, + rawIndex: rawIndex, + }, nil +} + +func (i *layoutIndex) Blob(h v1.Hash) (io.ReadCloser, error) { + return i.path.Blob(h) +} + +func (i *layoutIndex) findDescriptor(h v1.Hash) (*v1.Descriptor, error) { + im, err := i.IndexManifest() + if err != nil { + return nil, err + } + + if h == (v1.Hash{}) { + if len(im.Manifests) != 1 { + return nil, errors.New("oci layout must contain only a single image to be used with layout.Image") + } + return &(im.Manifests)[0], nil + } + + for _, desc := range im.Manifests { + if desc.Digest == h { + return &desc, nil + } + } + + return nil, fmt.Errorf("could not find descriptor in index: %s", h) +} + +// TODO: Pull this out into methods on types.MediaType? e.g. instead, have: +// * mt.IsIndex() +// * mt.IsImage() +func isExpectedMediaType(mt types.MediaType, expected ...types.MediaType) bool { + for _, allowed := range expected { + if mt == allowed { + return true + } + } + return false +} diff --git a/pkg/v1/layout/index_test.go b/pkg/v1/layout/index_test.go new file mode 100644 index 0000000..4478831 --- /dev/null +++ b/pkg/v1/layout/index_test.go @@ -0,0 +1,81 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestIndex(t *testing.T) { + idx, err := ImageIndexFromPath(testPath) + if err != nil { + t.Fatalf("ImageIndexFromPath() = %v", err) + } + + if err := validate.Index(idx); err != nil { + t.Errorf("validate.Index() = %v", err) + } + + mt, err := idx.MediaType() + if err != nil { + t.Fatalf("MediaType() = %v", err) + } + + if got, want := mt, types.OCIImageIndex; got != want { + t.Errorf("MediaType(); want: %v got: %v", want, got) + } + + indexHash, _ := v1.NewHash("sha256:2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb") + ii, err := idx.ImageIndex(indexHash) + if err != nil { + t.Fatalf("ImageIndex() = %v", err) + } + + mt, err = ii.MediaType() + if err != nil { + t.Fatalf("MediaType() = %v", err) + } + + if got, want := mt, types.DockerManifestList; got != want { + t.Errorf("MediaType(); want: %v got: %v", want, got) + } +} + +func TestIndexErrors(t *testing.T) { + idx, err := ImageIndexFromPath(testPath) + if err != nil { + t.Fatalf("ImageIndexFromPath() = %v", err) + } + + if _, err := idx.Image(bogusDigest); err == nil { + t.Errorf("idx.Image(%s) = nil, expected err", bogusDigest) + } + + if _, err := idx.Image(indexDigest); err == nil { + t.Errorf("idx.Image(%s) = nil, expected err", bogusDigest) + } + + if _, err := idx.ImageIndex(bogusDigest); err == nil { + t.Errorf("idx.ImageIndex(%s) = nil, expected err", bogusDigest) + } + + if _, err := idx.ImageIndex(manifestDigest); err == nil { + t.Errorf("idx.ImageIndex(%s) = nil, expected err", bogusDigest) + } +} diff --git a/pkg/v1/layout/layoutpath.go b/pkg/v1/layout/layoutpath.go new file mode 100644 index 0000000..a031ff5 --- /dev/null +++ b/pkg/v1/layout/layoutpath.go @@ -0,0 +1,25 @@ +// Copyright 2019 The original author or authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import "path/filepath" + +// Path represents an OCI image layout rooted in a file system path +type Path string + +func (l Path) path(elem ...string) string { + complete := []string{string(l)} + return filepath.Join(append(complete, elem...)...) +} diff --git a/pkg/v1/layout/options.go b/pkg/v1/layout/options.go new file mode 100644 index 0000000..a26f9f3 --- /dev/null +++ b/pkg/v1/layout/options.go @@ -0,0 +1,71 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import v1 "github.com/google/go-containerregistry/pkg/v1" + +// Option is a functional option for Layout. +type Option func(*options) + +type options struct { + descOpts []descriptorOption +} + +func makeOptions(opts ...Option) *options { + o := &options{ + descOpts: []descriptorOption{}, + } + for _, apply := range opts { + apply(o) + } + return o +} + +type descriptorOption func(*v1.Descriptor) + +// WithAnnotations adds annotations to the artifact descriptor. +func WithAnnotations(annotations map[string]string) Option { + return func(o *options) { + o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) { + if desc.Annotations == nil { + desc.Annotations = make(map[string]string) + } + for k, v := range annotations { + desc.Annotations[k] = v + } + }) + } +} + +// WithURLs adds urls to the artifact descriptor. +func WithURLs(urls []string) Option { + return func(o *options) { + o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) { + if desc.URLs == nil { + desc.URLs = []string{} + } + desc.URLs = append(desc.URLs, urls...) + }) + } +} + +// WithPlatform sets the platform of the artifact descriptor. +func WithPlatform(platform v1.Platform) Option { + return func(o *options) { + o.descOpts = append(o.descOpts, func(desc *v1.Descriptor) { + desc.Platform = &platform + }) + } +} diff --git a/pkg/v1/layout/read.go b/pkg/v1/layout/read.go new file mode 100644 index 0000000..796abc7 --- /dev/null +++ b/pkg/v1/layout/read.go @@ -0,0 +1,32 @@ +// Copyright 2019 The original author or authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "os" + "path/filepath" +) + +// FromPath reads an OCI image layout at path and constructs a layout.Path. +func FromPath(path string) (Path, error) { + // TODO: check oci-layout exists + + _, err := os.Stat(filepath.Join(path, "index.json")) + if err != nil { + return "", err + } + + return Path(path), nil +} diff --git a/pkg/v1/layout/read_test.go b/pkg/v1/layout/read_test.go new file mode 100644 index 0000000..281fa29 --- /dev/null +++ b/pkg/v1/layout/read_test.go @@ -0,0 +1,42 @@ +// Copyright 2019 The original author or authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "testing" +) + +func TestRead(t *testing.T) { + lp, err := FromPath(testPath) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + if testPath != lp.path() { + t.Errorf("unexpected path %s", lp.path()) + } +} + +func TestReadErrors(t *testing.T) { + if _, err := FromPath(bogusPath); err == nil { + t.Errorf("FromPath(%s) = nil, expected err", bogusPath) + } + + // Found this here: + // https://github.com/golang/go/issues/24195 + invalidPath := "double-null-padded-string\x00\x00" + if _, err := FromPath(invalidPath); err == nil { + t.Errorf("FromPath(%s) = nil, expected err", bogusPath) + } +} diff --git a/pkg/v1/layout/testdata/README.md b/pkg/v1/layout/testdata/README.md new file mode 100644 index 0000000..449ff1c --- /dev/null +++ b/pkg/v1/layout/testdata/README.md @@ -0,0 +1,5 @@ +# Where did this data come from? + +These were manually produced from the pkg/v1/tarball/testadata tarballs. + +TODO: Make this reproducible. There's not currently an easy way to do this. diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5 b/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5 new file mode 100644 index 0000000..1597d07 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5 @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 423, + "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650", + "annotations": { + "org.opencontainers.image.ref.name": "1" + } + } + ] +} diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb b/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb new file mode 100644 index 0000000..e6587e2 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 423, + "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650", + "annotations": { + "org.opencontainers.image.ref.name": "4" + } + } + ] +} diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3 b/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3 Binary files differnew file mode 100644 index 0000000..096f21f --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3 diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720 b/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720 new file mode 100644 index 0000000..48609c6 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":330,"digest":"sha256:930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":165,"digest":"sha256:321460fa87fd42433950b42d04b7aff249f4ed960d43404a9f699886906cc9d3"}]}
\ No newline at end of file diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e b/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e new file mode 100644 index 0000000..4228c89 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e @@ -0,0 +1 @@ +{"architecture": "amd64", "author": "Bazel", "config": {}, "created": "1970-01-01T00:00:00Z", "history": [{"author": "Bazel", "created": "1970-01-01T00:00:00Z", "created_by": "bazel build ..."}], "os": "linux", "rootfs": {"diff_ids": ["sha256:8897395fd26dc44ad0e2a834335b33198cb41ac4d98dfddf58eced3853fa7b17"], "type": "layers"}} diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9 b/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9 new file mode 100644 index 0000000..425c2d0 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/930705ce23e3b6ed4c08746b6fe880089c864fbaf62482702ae3fdd66b8c7fe9 @@ -0,0 +1 @@ +{"architecture": "amd64", "author": "Bazel", "config": {}, "created": "1970-01-01T00:00:00Z", "history": [{"author": "Bazel", "created": "1970-01-01T00:00:00Z", "created_by": "bazel build ..."}], "os": "linux", "rootfs": {"diff_ids": ["sha256:3610aa5267a210147ba6ca02cdd87610dfc08522de9c5f5015edd8ee14853fd8"], "type": "layers"}} diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b b/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b Binary files differnew file mode 100644 index 0000000..05c6321 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b diff --git a/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650 b/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650 new file mode 100644 index 0000000..21dc412 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/blobs/sha256/eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":330,"digest":"sha256:6e0b05049ed9c17d02e1a55e80d6599dbfcce7f4f4b022e3c673e685789c470e"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":167,"digest":"sha256:dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b"}]}
\ No newline at end of file diff --git a/pkg/v1/layout/testdata/test_index/index.json b/pkg/v1/layout/testdata/test_index/index.json new file mode 100644 index 0000000..28df736 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/index.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 423, + "digest": "sha256:eebff607b1628d67459b0596643fc07de70d702eccf030f0bc7bb6fc2b278650", + "annotations": { + "org.opencontainers.image.ref.name": "1" + } + }, + { + "mediaType": "application/vnd.oci.descriptor.v1+json", + "size": 423, + "digest": "sha256:32589985702551b6c56033bb3334432a0a513bf9d6aceda0f67c42b003850720", + "annotations": { + "org.opencontainers.image.ref.name": "2" + } + }, + { + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 314, + "digest": "sha256:05f95b26ed10668b7183c1e2da98610e91372fa9f510046d4ce5812addad86b5", + "annotations": { + "org.opencontainers.image.ref.name": "3" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "size": 314, + "digest": "sha256:2b29a2b8dea3af91ea7d0154be1da0c92d55ddd098540930fc8d3db7de377fdb", + "annotations": { + "org.opencontainers.image.ref.name": "4" + } + } + ] +} diff --git a/pkg/v1/layout/testdata/test_index/oci-layout b/pkg/v1/layout/testdata/test_index/oci-layout new file mode 100644 index 0000000..10ff2f3 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e new file mode 100644 index 0000000..53aea8f --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e @@ -0,0 +1,15 @@ +{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.helm.config.v1+json", + "digest": "sha256:ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356", + "size": 3 + }, + "layers": [ + { + "mediaType": "application/tar+gzip", + "digest": "sha256:dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b", + "size": 167 + } + ] +} diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356 @@ -0,0 +1 @@ +{} diff --git a/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b Binary files differnew file mode 100644 index 0000000..05c6321 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_media_type/blobs/sha256/dc52c6e48a1d51a96047b059f16889bc889c4b4c28f3b36b3f93187f62fc0b2b diff --git a/pkg/v1/layout/testdata/test_index_media_type/index.json b/pkg/v1/layout/testdata/test_index_media_type/index.json new file mode 100644 index 0000000..ffe4d3e --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_media_type/index.json @@ -0,0 +1,10 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 391, + "digest": "sha256:b544f71ecd82372bc9a3c0dbef378abfd2734fe437df81ff6e242a0d720d8e3e" + } + ] +} diff --git a/pkg/v1/layout/testdata/test_index_media_type/oci-layout b/pkg/v1/layout/testdata/test_index_media_type/oci-layout new file mode 100644 index 0000000..10ff2f3 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_media_type/oci-layout @@ -0,0 +1,3 @@ +{ + "imageLayoutVersion": "1.0.0" +} diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810 new file mode 100644 index 0000000..e49e018 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810 @@ -0,0 +1 @@ +{"created":"2020-04-12T10:58:48.626858334Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},"rootfs":{"type":"layers","diff_ids":["sha256:59cd31f50f7442a662d7c31b7a12079ade16892bfb465b33da49918e7d13e747"]},"history":[{"created":"2020-04-12T10:58:48.626858334Z","created_by":"/bin/sh -c #(nop) COPY file:a34f6c104b4cb0668083b4de122deebb3e3629e212f82c32fec316dd8e3a1931 in / "}]}
\ No newline at end of file diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0 Binary files differnew file mode 100644 index 0000000..1e4eb22 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0 diff --git a/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46 b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46 new file mode 100644 index 0000000..f02779b --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_one_image/blobs/sha256/98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46 @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:381d958b555884ba59574ab5c066e9f6116b5aec3567675aa13bec63331f0810","size":452},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:492b89b9dd3cda4596f94916d17f6901455fb8bd7f4c5a2a90df8d39c90f48a0","size":114}]}
\ No newline at end of file diff --git a/pkg/v1/layout/testdata/test_index_one_image/index.json b/pkg/v1/layout/testdata/test_index_one_image/index.json new file mode 100644 index 0000000..4ec03cc --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_one_image/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:98ceaf93e482fe91b9bfd6bba07137c098e49ee2d55e69f09fb6c951e75e0e46","size":344}]}
\ No newline at end of file diff --git a/pkg/v1/layout/testdata/test_index_one_image/oci-layout b/pkg/v1/layout/testdata/test_index_one_image/oci-layout new file mode 100644 index 0000000..21b1439 --- /dev/null +++ b/pkg/v1/layout/testdata/test_index_one_image/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"}
\ No newline at end of file diff --git a/pkg/v1/layout/write.go b/pkg/v1/layout/write.go new file mode 100644 index 0000000..906b12a --- /dev/null +++ b/pkg/v1/layout/write.go @@ -0,0 +1,481 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/types" + "golang.org/x/sync/errgroup" +) + +var layoutFile = `{ + "imageLayoutVersion": "1.0.0" +}` + +// AppendImage writes a v1.Image to the Path and updates +// the index.json to reference it. +func (l Path) AppendImage(img v1.Image, options ...Option) error { + if err := l.WriteImage(img); err != nil { + return err + } + + desc, err := partial.Descriptor(img) + if err != nil { + return err + } + + o := makeOptions(options...) + for _, opt := range o.descOpts { + opt(desc) + } + + return l.AppendDescriptor(*desc) +} + +// AppendIndex writes a v1.ImageIndex to the Path and updates +// the index.json to reference it. +func (l Path) AppendIndex(ii v1.ImageIndex, options ...Option) error { + if err := l.WriteIndex(ii); err != nil { + return err + } + + desc, err := partial.Descriptor(ii) + if err != nil { + return err + } + + o := makeOptions(options...) + for _, opt := range o.descOpts { + opt(desc) + } + + return l.AppendDescriptor(*desc) +} + +// AppendDescriptor adds a descriptor to the index.json of the Path. +func (l Path) AppendDescriptor(desc v1.Descriptor) error { + ii, err := l.ImageIndex() + if err != nil { + return err + } + + index, err := ii.IndexManifest() + if err != nil { + return err + } + + index.Manifests = append(index.Manifests, desc) + + rawIndex, err := json.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + return l.WriteFile("index.json", rawIndex, os.ModePerm) +} + +// ReplaceImage writes a v1.Image to the Path and updates +// the index.json to reference it, replacing any existing one that matches matcher, if found. +func (l Path) ReplaceImage(img v1.Image, matcher match.Matcher, options ...Option) error { + if err := l.WriteImage(img); err != nil { + return err + } + + return l.replaceDescriptor(img, matcher, options...) +} + +// ReplaceIndex writes a v1.ImageIndex to the Path and updates +// the index.json to reference it, replacing any existing one that matches matcher, if found. +func (l Path) ReplaceIndex(ii v1.ImageIndex, matcher match.Matcher, options ...Option) error { + if err := l.WriteIndex(ii); err != nil { + return err + } + + return l.replaceDescriptor(ii, matcher, options...) +} + +// replaceDescriptor adds a descriptor to the index.json of the Path, replacing +// any one matching matcher, if found. +func (l Path) replaceDescriptor(append mutate.Appendable, matcher match.Matcher, options ...Option) error { + ii, err := l.ImageIndex() + if err != nil { + return err + } + + desc, err := partial.Descriptor(append) + if err != nil { + return err + } + + o := makeOptions(options...) + for _, opt := range o.descOpts { + opt(desc) + } + + add := mutate.IndexAddendum{ + Add: append, + Descriptor: *desc, + } + ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add) + + index, err := ii.IndexManifest() + if err != nil { + return err + } + + rawIndex, err := json.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + return l.WriteFile("index.json", rawIndex, os.ModePerm) +} + +// RemoveDescriptors removes any descriptors that match the match.Matcher from the index.json of the Path. +func (l Path) RemoveDescriptors(matcher match.Matcher) error { + ii, err := l.ImageIndex() + if err != nil { + return err + } + ii = mutate.RemoveManifests(ii, matcher) + + index, err := ii.IndexManifest() + if err != nil { + return err + } + + rawIndex, err := json.MarshalIndent(index, "", " ") + if err != nil { + return err + } + + return l.WriteFile("index.json", rawIndex, os.ModePerm) +} + +// WriteFile write a file with arbitrary data at an arbitrary location in a v1 +// layout. Used mostly internally to write files like "oci-layout" and +// "index.json", also can be used to write other arbitrary files. Do *not* use +// this to write blobs. Use only WriteBlob() for that. +func (l Path) WriteFile(name string, data []byte, perm os.FileMode) error { + if err := os.MkdirAll(l.path(), os.ModePerm); err != nil && !os.IsExist(err) { + return err + } + + return os.WriteFile(l.path(name), data, perm) +} + +// WriteBlob copies a file to the blobs/ directory in the Path from the given ReadCloser at +// blobs/{hash.Algorithm}/{hash.Hex}. +func (l Path) WriteBlob(hash v1.Hash, r io.ReadCloser) error { + return l.writeBlob(hash, -1, r, nil) +} + +func (l Path) writeBlob(hash v1.Hash, size int64, rc io.ReadCloser, renamer func() (v1.Hash, error)) error { + if hash.Hex == "" && renamer == nil { + panic("writeBlob called an invalid hash and no renamer") + } + + dir := l.path("blobs", hash.Algorithm) + if err := os.MkdirAll(dir, os.ModePerm); err != nil && !os.IsExist(err) { + return err + } + + // Check if blob already exists and is the correct size + file := filepath.Join(dir, hash.Hex) + if s, err := os.Stat(file); err == nil && !s.IsDir() && (s.Size() == size || size == -1) { + return nil + } + + // If a renamer func was provided write to a temporary file + open := func() (*os.File, error) { return os.Create(file) } + if renamer != nil { + open = func() (*os.File, error) { return os.CreateTemp(dir, hash.Hex) } + } + w, err := open() + if err != nil { + return err + } + if renamer != nil { + // Delete temp file if an error is encountered before renaming + defer func() { + if err := os.Remove(w.Name()); err != nil && !errors.Is(err, os.ErrNotExist) { + logs.Warn.Printf("error removing temporary file after encountering an error while writing blob: %v", err) + } + }() + } + defer w.Close() + + // Write to file and exit if not renaming + if n, err := io.Copy(w, rc); err != nil || renamer == nil { + return err + } else if size != -1 && n != size { + return fmt.Errorf("expected blob size %d, but only wrote %d", size, n) + } + + // Always close reader before renaming, since Close computes the digest in + // the case of streaming layers. If Close is not called explicitly, it will + // occur in a goroutine that is not guaranteed to succeed before renamer is + // called. When renamer is the layer's Digest method, it can return + // ErrNotComputed. + if err := rc.Close(); err != nil { + return err + } + + // Always close file before renaming + if err := w.Close(); err != nil { + return err + } + + // Rename file based on the final hash + finalHash, err := renamer() + if err != nil { + return fmt.Errorf("error getting final digest of layer: %w", err) + } + + renamePath := l.path("blobs", finalHash.Algorithm, finalHash.Hex) + return os.Rename(w.Name(), renamePath) +} + +// writeLayer writes the compressed layer to a blob. Unlike WriteBlob it will +// write to a temporary file (suffixed with .tmp) within the layout until the +// compressed reader is fully consumed and written to disk. Also unlike +// WriteBlob, it will not skip writing and exit without error when a blob file +// exists, but does not have the correct size. (The blob hash is not +// considered, because it may be expensive to compute.) +func (l Path) writeLayer(layer v1.Layer) error { + d, err := layer.Digest() + if errors.Is(err, stream.ErrNotComputed) { + // Allow digest errors, since streams may not have calculated the hash + // yet. Instead, use an empty value, which will be transformed into a + // random file name with `os.CreateTemp` and the final digest will be + // calculated after writing to a temp file and before renaming to the + // final path. + d = v1.Hash{Algorithm: "sha256", Hex: ""} + } else if err != nil { + return err + } + + s, err := layer.Size() + if errors.Is(err, stream.ErrNotComputed) { + // Allow size errors, since streams may not have calculated the size + // yet. Instead, use zero as a sentinel value meaning that no size + // comparison can be done and any sized blob file should be considered + // valid and not overwritten. + // + // TODO: Provide an option to always overwrite blobs. + s = -1 + } else if err != nil { + return err + } + + r, err := layer.Compressed() + if err != nil { + return err + } + + if err := l.writeBlob(d, s, r, layer.Digest); err != nil { + return fmt.Errorf("error writing layer: %w", err) + } + return nil +} + +// RemoveBlob removes a file from the blobs directory in the Path +// at blobs/{hash.Algorithm}/{hash.Hex} +// It does *not* remove any reference to it from other manifests or indexes, or +// from the root index.json. +func (l Path) RemoveBlob(hash v1.Hash) error { + dir := l.path("blobs", hash.Algorithm) + err := os.Remove(filepath.Join(dir, hash.Hex)) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// WriteImage writes an image, including its manifest, config and all of its +// layers, to the blobs directory. If any blob already exists, as determined by +// the hash filename, does not write it. +// This function does *not* update the `index.json` file. If you want to write the +// image and also update the `index.json`, call AppendImage(), which wraps this +// and also updates the `index.json`. +func (l Path) WriteImage(img v1.Image) error { + layers, err := img.Layers() + if err != nil { + return err + } + + // Write the layers concurrently. + var g errgroup.Group + for _, layer := range layers { + layer := layer + g.Go(func() error { + return l.writeLayer(layer) + }) + } + if err := g.Wait(); err != nil { + return err + } + + // Write the config. + cfgName, err := img.ConfigName() + if err != nil { + return err + } + cfgBlob, err := img.RawConfigFile() + if err != nil { + return err + } + if err := l.WriteBlob(cfgName, io.NopCloser(bytes.NewReader(cfgBlob))); err != nil { + return err + } + + // Write the img manifest. + d, err := img.Digest() + if err != nil { + return err + } + manifest, err := img.RawManifest() + if err != nil { + return err + } + + return l.WriteBlob(d, io.NopCloser(bytes.NewReader(manifest))) +} + +type withLayer interface { + Layer(v1.Hash) (v1.Layer, error) +} + +type withBlob interface { + Blob(v1.Hash) (io.ReadCloser, error) +} + +func (l Path) writeIndexToFile(indexFile string, ii v1.ImageIndex) error { + index, err := ii.IndexManifest() + if err != nil { + return err + } + + // Walk the descriptors and write any v1.Image or v1.ImageIndex that we find. + // If we come across something we don't expect, just write it as a blob. + for _, desc := range index.Manifests { + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + ii, err := ii.ImageIndex(desc.Digest) + if err != nil { + return err + } + if err := l.WriteIndex(ii); err != nil { + return err + } + case types.OCIManifestSchema1, types.DockerManifestSchema2: + img, err := ii.Image(desc.Digest) + if err != nil { + return err + } + if err := l.WriteImage(img); err != nil { + return err + } + default: + // TODO: The layout could reference arbitrary things, which we should + // probably just pass through. + + var blob io.ReadCloser + // Workaround for #819. + if wl, ok := ii.(withLayer); ok { + layer, lerr := wl.Layer(desc.Digest) + if lerr != nil { + return lerr + } + blob, err = layer.Compressed() + } else if wb, ok := ii.(withBlob); ok { + blob, err = wb.Blob(desc.Digest) + } + if err != nil { + return err + } + if err := l.WriteBlob(desc.Digest, blob); err != nil { + return err + } + } + } + + rawIndex, err := ii.RawManifest() + if err != nil { + return err + } + + return l.WriteFile(indexFile, rawIndex, os.ModePerm) +} + +// WriteIndex writes an index to the blobs directory. Walks down the children, +// including its children manifests and/or indexes, and down the tree until all of +// config and all layers, have been written. If any blob already exists, as determined by +// the hash filename, does not write it. +// This function does *not* update the `index.json` file. If you want to write the +// index and also update the `index.json`, call AppendIndex(), which wraps this +// and also updates the `index.json`. +func (l Path) WriteIndex(ii v1.ImageIndex) error { + // Always just write oci-layout file, since it's small. + if err := l.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil { + return err + } + + h, err := ii.Digest() + if err != nil { + return err + } + + indexFile := filepath.Join("blobs", h.Algorithm, h.Hex) + return l.writeIndexToFile(indexFile, ii) +} + +// Write constructs a Path at path from an ImageIndex. +// +// The contents are written in the following format: +// At the top level, there is: +// +// One oci-layout file containing the version of this image-layout. +// One index.json file listing descriptors for the contained images. +// +// Under blobs/, there is, for each image: +// +// One file for each layer, named after the layer's SHA. +// One file for each config blob, named after its SHA. +// One file for each manifest blob, named after its SHA. +func Write(path string, ii v1.ImageIndex) (Path, error) { + lp := Path(path) + // Always just write oci-layout file, since it's small. + if err := lp.WriteFile("oci-layout", []byte(layoutFile), os.ModePerm); err != nil { + return "", err + } + + // TODO create blobs/ in case there is a blobs file which would prevent the directory from being created + + return lp, lp.writeIndexToFile("index.json", ii) +} diff --git a/pkg/v1/layout/write_test.go b/pkg/v1/layout/write_test.go new file mode 100644 index 0000000..530e0e8 --- /dev/null +++ b/pkg/v1/layout/write_test.go @@ -0,0 +1,672 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package layout + +import ( + "archive/tar" + "bytes" + "io" + "log" + "os" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestWrite(t *testing.T) { + tmp := t.TempDir() + + original, err := ImageIndexFromPath(testPath) + if err != nil { + t.Fatal(err) + } + + if layoutPath, err := Write(tmp, original); err != nil { + t.Fatalf("Write(%s) = %v", tmp, err) + } else if tmp != layoutPath.path() { + t.Fatalf("unexpected file system path %v", layoutPath) + } + + written, err := ImageIndexFromPath(tmp) + if err != nil { + t.Fatal(err) + } + + if err := validate.Index(written); err != nil { + t.Fatalf("validate.Index() = %v", err) + } +} + +func TestWriteErrors(t *testing.T) { + idx, err := ImageIndexFromPath(testPath) + if err != nil { + t.Fatalf("ImageIndexFromPath() = %v", err) + } + + // Found this here: + // https://github.com/golang/go/issues/24195 + invalidPath := "double-null-padded-string\x00\x00" + if _, err := Write(invalidPath, idx); err == nil { + t.Fatalf("Write(%s) = nil, expected err", invalidPath) + } +} + +func TestAppendDescriptorInitializesIndex(t *testing.T) { + tmp := t.TempDir() + temp, err := Write(tmp, empty.Index) + if err != nil { + t.Fatal(err) + } + + // Append a descriptor to a non-existent layout. + desc := v1.Descriptor{ + Digest: bogusDigest, + Size: 1337, + MediaType: types.MediaType("not real"), + } + if err := temp.AppendDescriptor(desc); err != nil { + t.Fatalf("AppendDescriptor(%s) = %v", tmp, err) + } + + // Read that layout from disk and make sure the descriptor is there. + idx, err := ImageIndexFromPath(tmp) + if err != nil { + t.Fatalf("ImageIndexFromPath() = %v", err) + } + + manifest, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest() = %v", err) + } + if diff := cmp.Diff(manifest.Manifests[0], desc); diff != "" { + t.Fatalf("bad descriptor: (-got +want) %s", diff) + } +} + +func TestRoundtrip(t *testing.T) { + tmp := t.TempDir() + + original, err := ImageIndexFromPath(testPath) + if err != nil { + t.Fatal(err) + } + + originalManifest, err := original.IndexManifest() + if err != nil { + t.Fatal(err) + } + + // Write it back. + if _, err := Write(tmp, original); err != nil { + t.Fatal(err) + } + reconstructed, err := ImageIndexFromPath(tmp) + if err != nil { + t.Fatalf("ImageIndexFromPath() = %v", err) + } + reconstructedManifest, err := reconstructed.IndexManifest() + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(originalManifest, reconstructedManifest); diff != "" { + t.Fatalf("bad manifest: (-got +want) %s", diff) + } +} + +func TestOptions(t *testing.T) { + tmp := t.TempDir() + temp, err := Write(tmp, empty.Index) + if err != nil { + t.Fatal(err) + } + annotations := map[string]string{ + "foo": "bar", + } + urls := []string{"https://example.com"} + platform := v1.Platform{ + Architecture: "mill", + OS: "haiku", + } + img, err := random.Image(5, 5) + if err != nil { + t.Fatal(err) + } + options := []Option{ + WithAnnotations(annotations), + WithURLs(urls), + WithPlatform(platform), + } + if err := temp.AppendImage(img, options...); err != nil { + t.Fatal(err) + } + idx, err := temp.ImageIndex() + if err != nil { + t.Fatal(err) + } + indexManifest, err := idx.IndexManifest() + if err != nil { + t.Fatal(err) + } + + desc := indexManifest.Manifests[0] + if got, want := desc.Annotations["foo"], "bar"; got != want { + t.Fatalf("wrong annotation; got: %v, want: %v", got, want) + } + if got, want := desc.URLs[0], "https://example.com"; got != want { + t.Fatalf("wrong urls; got: %v, want: %v", got, want) + } + if got, want := desc.Platform.Architecture, "mill"; got != want { + t.Fatalf("wrong Architecture; got: %v, want: %v", got, want) + } + if got, want := desc.Platform.OS, "haiku"; got != want { + t.Fatalf("wrong OS; got: %v, want: %v", got, want) + } +} + +func TestDeduplicatedWrites(t *testing.T) { + lp, err := FromPath(testPath) + if err != nil { + t.Fatalf("FromPath() = %v", err) + } + + b, err := lp.Blob(configDigest) + if err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer([]byte{}) + if _, err := io.Copy(buf, b); err != nil { + log.Fatal(err) + } + + if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil { + t.Fatal(err) + } + + if err := lp.WriteBlob(configDigest, io.NopCloser(bytes.NewBuffer(buf.Bytes()))); err != nil { + t.Fatal(err) + } +} + +func TestRemoveDescriptor(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two images + image1, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image1); err != nil { + t.Fatal(err) + } + image2, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image2); err != nil { + t.Fatal(err) + } + + // remove one of the images by descriptor and ensure it is correct + digest1, err := image1.Digest() + if err != nil { + t.Fatal(err) + } + digest2, err := image2.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.RemoveDescriptors(match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 1 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 1) + } + if manifest.Manifests[0].Digest != digest2 { + t.Fatal("removed wrong digest") + } +} + +func TestReplaceIndex(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two indexes + index1, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendIndex(index1); err != nil { + t.Fatal(err) + } + index2, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendIndex(index2); err != nil { + t.Fatal(err) + } + index3, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + + // remove one of the indexes by descriptor and ensure it is correct + digest1, err := index1.Digest() + if err != nil { + t.Fatal(err) + } + digest3, err := index3.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.ReplaceIndex(index3, match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 2 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2) + } + // we should have digest3, and *not* have digest1 + var have3 bool + for _, m := range manifest.Manifests { + if m.Digest == digest1 { + t.Fatal("found digest1 still not replaced", digest1) + } + if m.Digest == digest3 { + have3 = true + } + } + if !have3 { + t.Fatal("could not find digest3", digest3) + } +} + +func TestReplaceImage(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex + ii = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // add two images + image1, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image1); err != nil { + t.Fatal(err) + } + image2, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + if err := l.AppendImage(image2); err != nil { + t.Fatal(err) + } + image3, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + + // remove one of the images by descriptor and ensure it is correct + digest1, err := image1.Digest() + if err != nil { + t.Fatal(err) + } + digest3, err := image3.Digest() + if err != nil { + t.Fatal(err) + } + if err := l.ReplaceImage(image3, match.Digests(digest1)); err != nil { + t.Fatal(err) + } + // ensure we only have one + ii, err = l.ImageIndex() + if err != nil { + t.Fatal(err) + } + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != 2 { + t.Fatalf("mismatched manifests count, had %d, expected %d", len(manifest.Manifests), 2) + } + // we should have digest3, and *not* have digest1 + var have3 bool + for _, m := range manifest.Manifests { + if m.Digest == digest1 { + t.Fatal("found digest1 still not replaced", digest1) + } + if m.Digest == digest3 { + have3 = true + } + } + if !have3 { + t.Fatal("could not find digest3", digest3) + } +} + +func TestRemoveBlob(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // create a random blob + b := []byte("abcdefghijklmnop") + hash, _, err := v1.SHA256(bytes.NewReader(b)) + if err != nil { + t.Fatal(err) + } + + if err := l.WriteBlob(hash, io.NopCloser(bytes.NewReader(b))); err != nil { + t.Fatal(err) + } + // make sure it exists + b2, err := l.Bytes(hash) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(b, b2) { + t.Fatal("mismatched bytes") + } + // now the real test, delete it + if err := l.RemoveBlob(hash); err != nil { + t.Fatal(err) + } + // now it should not exist + if _, err = l.Bytes(hash); err == nil { + t.Fatal("still existed after deletion") + } +} + +func TestStreamingWriteLayer(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // create a random streaming image and persist + pr, pw := io.Pipe() + tw := tar.NewWriter(pw) + go func() { + pw.CloseWithError(func() error { + body := "test file" + if err := tw.WriteHeader(&tar.Header{ + Name: "test.txt", + Mode: 0600, + Size: int64(len(body)), + Typeflag: tar.TypeReg, + }); err != nil { + return err + } + if _, err := tw.Write([]byte(body)); err != nil { + return err + } + return tw.Close() + }()) + }() + img, err := mutate.Append(empty.Image, mutate.Addendum{ + Layer: stream.NewLayer(pr), + }) + if err != nil { + t.Fatalf("creating random streaming image failed: %v", err) + } + if _, err := img.Digest(); err == nil { + t.Fatal("digesting image before stream is consumed; (v1.Image).Digest() = nil, expected err") + } + // AppendImage uses writeLayer + if err := l.AppendImage(img); err != nil { + t.Fatalf("(Path).AppendImage() = %v", err) + } + + // Check that image was persisted and is valid + imgDigest, err := img.Digest() + if err != nil { + t.Fatalf("(v1.Image).Digest() = %v", err) + } + img, err = l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading image after writeLayer for validation; (Path).Image = %v", err) + } + if err := validate.Image(img); err != nil { + t.Fatalf("validate.Image() = %v", err) + } +} + +func TestOverwriteWithWriteLayer(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // create a random image and persist + img, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + imgDigest, err := img.Digest() + if err != nil { + t.Fatalf("(v1.Image).Digest() = %v", err) + } + if err := l.AppendImage(img); err != nil { + t.Fatalf("(Path).AppendImage() = %v", err) + } + if err := validate.Image(img); err != nil { + t.Fatalf("validate.Image() = %v", err) + } + + // get the random image's layer + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + if n := len(layers); n != 1 { + t.Fatalf("expected image with 1 layer, got %d", n) + } + + layer := layers[0] + layerDigest, err := layer.Digest() + if err != nil { + t.Fatalf("(v1.Layer).Digest() = %v", err) + } + + // truncate the layer contents on disk + completeLayerBytes, err := l.Bytes(layerDigest) + if err != nil { + t.Fatalf("(Path).Bytes() = %v", err) + } + truncatedLayerBytes := completeLayerBytes[:512] + + path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex) + if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil { + t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err) + } + + // ensure validation fails + img, err = l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err) + } + if err := validate.Image(img); err == nil { + t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err") + } + + // try writing expected contents with WriteBlob + if err := l.WriteBlob(layerDigest, io.NopCloser(bytes.NewBuffer(completeLayerBytes))); err != nil { + t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).WriteBlob = %v", err) + } + + // validation should still fail + img, err = l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading truncated image after WriteBlob for validation; (Path).Image = %v", err) + } + if err := validate.Image(img); err == nil { + t.Fatal("validating image after attempting repair of truncated layer with WriteBlob; validate.Image() = nil, expected err") + } + + // try writing expected contents with writeLayer + if err := l.writeLayer(layer); err != nil { + t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).writeLayer = %v", err) + } + + // validation should now succeed + img, err = l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading truncated image after writeLayer for validation; (Path).Image = %v", err) + } + if err := validate.Image(img); err != nil { + t.Fatalf("validating image after attempting repair of truncated layer with writeLayer; validate.Image() = %v", err) + } +} + +func TestOverwriteWithReplaceImage(t *testing.T) { + // need to set up a basic path + tmp := t.TempDir() + + var ii v1.ImageIndex = empty.Index + l, err := Write(tmp, ii) + if err != nil { + t.Fatal(err) + } + + // create a random image and persist + img, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + imgDigest, err := img.Digest() + if err != nil { + t.Fatalf("(v1.Image).Digest() = %v", err) + } + if err := l.AppendImage(img); err != nil { + t.Fatalf("(Path).AppendImage() = %v", err) + } + if err := validate.Image(img); err != nil { + t.Fatalf("validate.Image() = %v", err) + } + + // get the random image's layer + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + if n := len(layers); n != 1 { + t.Fatalf("expected image with 1 layer, got %d", n) + } + + layer := layers[0] + layerDigest, err := layer.Digest() + if err != nil { + t.Fatalf("(v1.Layer).Digest() = %v", err) + } + + // truncate the layer contents on disk + completeLayerBytes, err := l.Bytes(layerDigest) + if err != nil { + t.Fatalf("(Path).Bytes() = %v", err) + } + truncatedLayerBytes := completeLayerBytes[:512] + + path := l.path("blobs", layerDigest.Algorithm, layerDigest.Hex) + if err := os.WriteFile(path, truncatedLayerBytes, os.ModePerm); err != nil { + t.Fatalf("os.WriteFile(layerPath, truncated) = %v", err) + } + + // ensure validation fails + truncatedImg, err := l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading truncated image for validation; (Path).Image = %v", err) + } + if err := validate.Image(truncatedImg); err == nil { + t.Fatal("validating image after truncating layer; validate.Image() = nil, expected err") + } else if strings.Contains(err.Error(), "unexpected EOF") { + t.Fatalf("validating image after truncating layer; validate.Image() error is not helpful: %v", err) + } + + // try writing expected contents with ReplaceImage + if err := l.ReplaceImage(img, match.Digests(imgDigest)); err != nil { + t.Fatalf("error attempting to overwrite truncated layer with valid layer; (Path).ReplaceImage = %v", err) + } + + // validation should now succeed + repairedImg, err := l.Image(imgDigest) + if err != nil { + t.Fatalf("error loading truncated image after ReplaceImage for validation; (Path).Image = %v", err) + } + if err := validate.Image(repairedImg); err != nil { + t.Fatalf("validating image after attempting repair of truncated layer with ReplaceImage; validate.Image() = %v", err) + } +} diff --git a/pkg/v1/manifest.go b/pkg/v1/manifest.go new file mode 100644 index 0000000..22d483f --- /dev/null +++ b/pkg/v1/manifest.go @@ -0,0 +1,71 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "encoding/json" + "io" + + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Manifest represents the OCI image manifest in a structured way. +type Manifest struct { + SchemaVersion int64 `json:"schemaVersion"` + MediaType types.MediaType `json:"mediaType,omitempty"` + Config Descriptor `json:"config"` + Layers []Descriptor `json:"layers"` + Annotations map[string]string `json:"annotations,omitempty"` + Subject *Descriptor `json:"subject,omitempty"` +} + +// IndexManifest represents an OCI image index in a structured way. +type IndexManifest struct { + SchemaVersion int64 `json:"schemaVersion"` + MediaType types.MediaType `json:"mediaType,omitempty"` + Manifests []Descriptor `json:"manifests"` + Annotations map[string]string `json:"annotations,omitempty"` + Subject *Descriptor `json:"subject,omitempty"` +} + +// Descriptor holds a reference from the manifest to one of its constituent elements. +type Descriptor struct { + MediaType types.MediaType `json:"mediaType"` + Size int64 `json:"size"` + Digest Hash `json:"digest"` + Data []byte `json:"data,omitempty"` + URLs []string `json:"urls,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` + Platform *Platform `json:"platform,omitempty"` + ArtifactType string `json:"artifactType,omitempty"` +} + +// ParseManifest parses the io.Reader's contents into a Manifest. +func ParseManifest(r io.Reader) (*Manifest, error) { + m := Manifest{} + if err := json.NewDecoder(r).Decode(&m); err != nil { + return nil, err + } + return &m, nil +} + +// ParseIndexManifest parses the io.Reader's contents into an IndexManifest. +func ParseIndexManifest(r io.Reader) (*IndexManifest, error) { + im := IndexManifest{} + if err := json.NewDecoder(r).Decode(&im); err != nil { + return nil, err + } + return &im, nil +} diff --git a/pkg/v1/manifest_test.go b/pkg/v1/manifest_test.go new file mode 100644 index 0000000..5cd5526 --- /dev/null +++ b/pkg/v1/manifest_test.go @@ -0,0 +1,76 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestGoodManifestSimple(t *testing.T) { + got, err := ParseManifest(strings.NewReader(`{}`)) + if err != nil { + t.Errorf("Unexpected error parsing manifest: %v", err) + } + + want := Manifest{} + if diff := cmp.Diff(want, *got); diff != "" { + t.Errorf("ParseManifest({}); (-want +got) %s", diff) + } +} + +func TestGoodManifestWithHash(t *testing.T) { + good, err := ParseManifest(strings.NewReader(`{ + "config": { + "digest": "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + } +}`)) + if err != nil { + t.Errorf("Unexpected error parsing manifest: %v", err) + } + + if got, want := good.Config.Digest.Algorithm, "sha256"; got != want { + t.Errorf("ParseManifest().Config.Digest.Algorithm; got %v, want %v", got, want) + } +} + +func TestManifestWithBadHash(t *testing.T) { + bad, err := ParseManifest(strings.NewReader(`{ + "config": { + "digest": "sha256:deadbeed" + } +}`)) + if err == nil { + t.Errorf("Expected error parsing manifest, but got: %v", bad) + } +} + +func TestParseIndexManifest(t *testing.T) { + got, err := ParseIndexManifest(strings.NewReader(`{}`)) + if err != nil { + t.Errorf("Unexpected error parsing manifest: %v", err) + } + + want := IndexManifest{} + if diff := cmp.Diff(want, *got); diff != "" { + t.Errorf("ParseIndexManifest({}); (-want +got) %s", diff) + } + + if got, err := ParseIndexManifest(strings.NewReader("{")); err == nil { + t.Errorf("expected error, got: %v", got) + } +} diff --git a/pkg/v1/match/match.go b/pkg/v1/match/match.go new file mode 100644 index 0000000..98b1ff9 --- /dev/null +++ b/pkg/v1/match/match.go @@ -0,0 +1,92 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package match provides functionality for conveniently matching a v1.Descriptor. +package match + +import ( + v1 "github.com/google/go-containerregistry/pkg/v1" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Matcher function that is given a v1.Descriptor, and returns whether or +// not it matches a given rule. Can match on anything it wants in the Descriptor. +type Matcher func(desc v1.Descriptor) bool + +// Name returns a match.Matcher that matches based on the value of the +// +// "org.opencontainers.image.ref.name" annotation: +// +// github.com/opencontainers/image-spec/blob/v1.0.1/annotations.md#pre-defined-annotation-keys +func Name(name string) Matcher { + return Annotation(imagespec.AnnotationRefName, name) +} + +// Annotation returns a match.Matcher that matches based on the provided annotation. +func Annotation(key, value string) Matcher { + return func(desc v1.Descriptor) bool { + if desc.Annotations == nil { + return false + } + if aValue, ok := desc.Annotations[key]; ok && aValue == value { + return true + } + return false + } +} + +// Platforms returns a match.Matcher that matches on any one of the provided platforms. +// Ignores any descriptors that do not have a platform. +func Platforms(platforms ...v1.Platform) Matcher { + return func(desc v1.Descriptor) bool { + if desc.Platform == nil { + return false + } + for _, platform := range platforms { + if desc.Platform.Equals(platform) { + return true + } + } + return false + } +} + +// MediaTypes returns a match.Matcher that matches at least one of the provided media types. +func MediaTypes(mediaTypes ...string) Matcher { + mts := map[string]bool{} + for _, media := range mediaTypes { + mts[media] = true + } + return func(desc v1.Descriptor) bool { + if desc.MediaType == "" { + return false + } + if _, ok := mts[string(desc.MediaType)]; ok { + return true + } + return false + } +} + +// Digests returns a match.Matcher that matches at least one of the provided Digests +func Digests(digests ...v1.Hash) Matcher { + digs := map[v1.Hash]bool{} + for _, digest := range digests { + digs[digest] = true + } + return func(desc v1.Descriptor) bool { + _, ok := digs[desc.Digest] + return ok + } +} diff --git a/pkg/v1/match/match_test.go b/pkg/v1/match/match_test.go new file mode 100644 index 0000000..c54319d --- /dev/null +++ b/pkg/v1/match/match_test.go @@ -0,0 +1,131 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package match_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/types" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" +) + +func TestName(t *testing.T) { + tests := []struct { + desc v1.Descriptor + name string + match bool + }{ + {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "foo", true}, + {v1.Descriptor{Annotations: map[string]string{imagespec.AnnotationRefName: "foo"}}, "bar", false}, + {v1.Descriptor{Annotations: map[string]string{}}, "bar", false}, + {v1.Descriptor{Annotations: nil}, "bar", false}, + {v1.Descriptor{}, "bar", false}, + } + for i, tt := range tests { + f := match.Name(tt.name) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v name %s", i, match, tt.match, tt.desc, tt.name) + } + } +} + +func TestAnnotation(t *testing.T) { + tests := []struct { + desc v1.Descriptor + key string + value string + match bool + }{ + {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "foo", "bar", true}, + {v1.Descriptor{Annotations: map[string]string{"foo": "bar"}}, "bar", "foo", false}, + {v1.Descriptor{Annotations: map[string]string{}}, "foo", "bar", false}, + {v1.Descriptor{Annotations: nil}, "foo", "bar", false}, + {v1.Descriptor{}, "foo", "bar", false}, + } + for i, tt := range tests { + f := match.Annotation(tt.key, tt.value) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v annotation %s:%s", i, match, tt.match, tt.desc, tt.key, tt.value) + } + } +} + +func TestPlatforms(t *testing.T) { + tests := []struct { + desc v1.Descriptor + platforms []v1.Platform + match bool + }{ + {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, []v1.Platform{{Architecture: "amd64", OS: "darwin"}, {Architecture: "amd64", OS: "linux"}}, true}, + {v1.Descriptor{Platform: &v1.Platform{Architecture: "amd64", OS: "linux"}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}, {Architecture: "s390x", OS: "linux"}}, false}, + {v1.Descriptor{Platform: &v1.Platform{OS: "linux"}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false}, + {v1.Descriptor{Platform: &v1.Platform{}}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false}, + {v1.Descriptor{Platform: nil}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false}, + {v1.Descriptor{}, []v1.Platform{{Architecture: "arm64", OS: "linux"}}, false}, + } + for i, tt := range tests { + f := match.Platforms(tt.platforms...) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v platform %#v", i, match, tt.match, tt.desc, tt.platforms) + } + } +} + +func TestMediaTypes(t *testing.T) { + tests := []struct { + desc v1.Descriptor + mediaTypes []string + match bool + }{ + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIImageIndex)}, true}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1)}, false}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, true}, + {v1.Descriptor{MediaType: types.OCIImageIndex}, []string{"a", "b"}, false}, + {v1.Descriptor{}, []string{string(types.OCIManifestSchema1), string(types.OCIImageIndex)}, false}, + } + for i, tt := range tests { + f := match.MediaTypes(tt.mediaTypes...) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v mediaTypes %#v", i, match, tt.match, tt.desc, tt.mediaTypes) + } + } +} + +func TestDigests(t *testing.T) { + hashes := []string{ + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "abcde1111111222f0123456789abcdef0123456789abcdef0123456789abcdef", + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + } + algo := "sha256" + + tests := []struct { + desc v1.Descriptor + digests []v1.Hash + match bool + }{ + {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[0]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, true}, + {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[1]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, true}, + {v1.Descriptor{Digest: v1.Hash{Algorithm: algo, Hex: hashes[2]}}, []v1.Hash{{Algorithm: algo, Hex: hashes[0]}, {Algorithm: algo, Hex: hashes[1]}}, false}, + } + for i, tt := range tests { + f := match.Digests(tt.digests...) + if match := f(tt.desc); match != tt.match { + t.Errorf("%d: mismatched, got %v expected %v for desc %#v digests %#v", i, match, tt.match, tt.desc, tt.digests) + } + } +} diff --git a/pkg/v1/mutate/README.md b/pkg/v1/mutate/README.md new file mode 100644 index 0000000..19e1612 --- /dev/null +++ b/pkg/v1/mutate/README.md @@ -0,0 +1,56 @@ +# `mutate` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/mutate) + +The `v1.Image`, `v1.ImageIndex`, and `v1.Layer` interfaces provide only +accessor methods, so they are essentially immutable. If you want to change +something about them, you need to produce a new instance of that interface. + +A common use case for this library is to read an image from somewhere (a source), +change something about it, and write the image somewhere else (a sink). + +Graphically, this looks something like: + +<p align="center"> + <img src="/images/mutate.dot.svg" /> +</p> + +## Mutations + +This is obviously not a comprehensive set of useful transformations (PRs welcome!), +but a rough summary of what the `mutate` package currently does: + +### `Config` and `ConfigFile` + +These allow you to change the [image configuration](https://github.com/opencontainers/image-spec/blob/master/config.md#properties), +e.g. to change the entrypoint, environment, author, etc. + +### `Time`, `Canonical`, and `CreatedAt` + +These are useful in the context of [reproducible builds](https://reproducible-builds.org/), +where you may want to strip timestamps and other non-reproducible information. + +### `Append`, `AppendLayers`, and `AppendManifests` + +These functions allow the extension of a `v1.Image` or `v1.ImageIndex` with +new layers or manifests. + +For constructing an image `FROM scratch`, see the [`empty`](/pkg/v1/empty) package. + +### `MediaType` and `IndexMediaType` + +Sometimes, it is necessary to change the media type of an image or index, +e.g. to appease a registry with strict validation of images (_looking at you, GCR_). + +### `Rebase` + +Rebase has [its own README](/cmd/crane/rebase.md). + +This is the underlying implementation of [`crane rebase`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_rebase.md). + +### `Extract` + +Extract will flatten an image filesystem into a single tar stream, +respecting whiteout files. + +This is the underlying implementation of [`crane export`](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane_export.md). diff --git a/pkg/v1/mutate/doc.go b/pkg/v1/mutate/doc.go new file mode 100644 index 0000000..dfbd995 --- /dev/null +++ b/pkg/v1/mutate/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mutate provides facilities for mutating v1.Images of any kind. +package mutate diff --git a/pkg/v1/mutate/image.go b/pkg/v1/mutate/image.go new file mode 100644 index 0000000..727abe2 --- /dev/null +++ b/pkg/v1/mutate/image.go @@ -0,0 +1,287 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate + +import ( + "bytes" + "encoding/json" + "errors" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type image struct { + base v1.Image + adds []Addendum + + computed bool + configFile *v1.ConfigFile + manifest *v1.Manifest + annotations map[string]string + mediaType *types.MediaType + configMediaType *types.MediaType + diffIDMap map[v1.Hash]v1.Layer + digestMap map[v1.Hash]v1.Layer + subject *v1.Descriptor +} + +var _ v1.Image = (*image)(nil) + +func (i *image) MediaType() (types.MediaType, error) { + if i.mediaType != nil { + return *i.mediaType, nil + } + return i.base.MediaType() +} + +func (i *image) compute() error { + // Don't re-compute if already computed. + if i.computed { + return nil + } + var configFile *v1.ConfigFile + if i.configFile != nil { + configFile = i.configFile + } else { + cf, err := i.base.ConfigFile() + if err != nil { + return err + } + configFile = cf.DeepCopy() + } + diffIDs := configFile.RootFS.DiffIDs + history := configFile.History + + diffIDMap := make(map[v1.Hash]v1.Layer) + digestMap := make(map[v1.Hash]v1.Layer) + + for _, add := range i.adds { + history = append(history, add.History) + if add.Layer != nil { + diffID, err := add.Layer.DiffID() + if err != nil { + return err + } + diffIDs = append(diffIDs, diffID) + diffIDMap[diffID] = add.Layer + } + } + + m, err := i.base.Manifest() + if err != nil { + return err + } + manifest := m.DeepCopy() + manifestLayers := manifest.Layers + for _, add := range i.adds { + if add.Layer == nil { + // Empty layers include only history in manifest. + continue + } + + desc, err := partial.Descriptor(add.Layer) + if err != nil { + return err + } + + // Fields in the addendum override the original descriptor. + if len(add.Annotations) != 0 { + desc.Annotations = add.Annotations + } + if len(add.URLs) != 0 { + desc.URLs = add.URLs + } + + if add.MediaType != "" { + desc.MediaType = add.MediaType + } + + manifestLayers = append(manifestLayers, *desc) + digestMap[desc.Digest] = add.Layer + } + + configFile.RootFS.DiffIDs = diffIDs + configFile.History = history + + manifest.Layers = manifestLayers + + rcfg, err := json.Marshal(configFile) + if err != nil { + return err + } + d, sz, err := v1.SHA256(bytes.NewBuffer(rcfg)) + if err != nil { + return err + } + manifest.Config.Digest = d + manifest.Config.Size = sz + + // If Data was set in the base image, we need to update it in the mutated image. + if m.Config.Data != nil { + manifest.Config.Data = rcfg + } + + // If the user wants to mutate the media type of the config + if i.configMediaType != nil { + manifest.Config.MediaType = *i.configMediaType + } + + if i.mediaType != nil { + manifest.MediaType = *i.mediaType + } + + if i.annotations != nil { + if manifest.Annotations == nil { + manifest.Annotations = map[string]string{} + } + + for k, v := range i.annotations { + manifest.Annotations[k] = v + } + } + manifest.Subject = i.subject + + i.configFile = configFile + i.manifest = manifest + i.diffIDMap = diffIDMap + i.digestMap = digestMap + i.computed = true + return nil +} + +// Layers returns the ordered collection of filesystem layers that comprise this image. +// The order of the list is oldest/base layer first, and most-recent/top layer last. +func (i *image) Layers() ([]v1.Layer, error) { + if err := i.compute(); errors.Is(err, stream.ErrNotComputed) { + // Image contains a streamable layer which has not yet been + // consumed. Just return the layers we have in case the caller + // is going to consume the layers. + layers, err := i.base.Layers() + if err != nil { + return nil, err + } + for _, add := range i.adds { + layers = append(layers, add.Layer) + } + return layers, nil + } else if err != nil { + return nil, err + } + + diffIDs, err := partial.DiffIDs(i) + if err != nil { + return nil, err + } + ls := make([]v1.Layer, 0, len(diffIDs)) + for _, h := range diffIDs { + l, err := i.LayerByDiffID(h) + if err != nil { + return nil, err + } + ls = append(ls, l) + } + return ls, nil +} + +// ConfigName returns the hash of the image's config file. +func (i *image) ConfigName() (v1.Hash, error) { + if err := i.compute(); err != nil { + return v1.Hash{}, err + } + return partial.ConfigName(i) +} + +// ConfigFile returns this image's config file. +func (i *image) ConfigFile() (*v1.ConfigFile, error) { + if err := i.compute(); err != nil { + return nil, err + } + return i.configFile.DeepCopy(), nil +} + +// RawConfigFile returns the serialized bytes of ConfigFile() +func (i *image) RawConfigFile() ([]byte, error) { + if err := i.compute(); err != nil { + return nil, err + } + return json.Marshal(i.configFile) +} + +// Digest returns the sha256 of this image's manifest. +func (i *image) Digest() (v1.Hash, error) { + if err := i.compute(); err != nil { + return v1.Hash{}, err + } + return partial.Digest(i) +} + +// Size implements v1.Image. +func (i *image) Size() (int64, error) { + if err := i.compute(); err != nil { + return -1, err + } + return partial.Size(i) +} + +// Manifest returns this image's Manifest object. +func (i *image) Manifest() (*v1.Manifest, error) { + if err := i.compute(); err != nil { + return nil, err + } + return i.manifest.DeepCopy(), nil +} + +// RawManifest returns the serialized bytes of Manifest() +func (i *image) RawManifest() ([]byte, error) { + if err := i.compute(); err != nil { + return nil, err + } + return json.Marshal(i.manifest) +} + +// LayerByDigest returns a Layer for interacting with a particular layer of +// the image, looking it up by "digest" (the compressed hash). +func (i *image) LayerByDigest(h v1.Hash) (v1.Layer, error) { + if cn, err := i.ConfigName(); err != nil { + return nil, err + } else if h == cn { + return partial.ConfigLayer(i) + } + if layer, ok := i.digestMap[h]; ok { + return layer, nil + } + return i.base.LayerByDigest(h) +} + +// LayerByDiffID is an analog to LayerByDigest, looking up by "diff id" +// (the uncompressed hash). +func (i *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) { + if layer, ok := i.diffIDMap[h]; ok { + return layer, nil + } + return i.base.LayerByDiffID(h) +} + +func validate(adds []Addendum) error { + for _, add := range adds { + if add.Layer == nil && !add.History.EmptyLayer { + return errors.New("unable to add a nil layer to the image") + } + } + return nil +} diff --git a/pkg/v1/mutate/index.go b/pkg/v1/mutate/index.go new file mode 100644 index 0000000..ba062f9 --- /dev/null +++ b/pkg/v1/mutate/index.go @@ -0,0 +1,204 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate + +import ( + "encoding/json" + "fmt" + + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func computeDescriptor(ia IndexAddendum) (*v1.Descriptor, error) { + desc, err := partial.Descriptor(ia.Add) + if err != nil { + return nil, err + } + + // The IndexAddendum allows overriding Descriptor values. + if ia.Descriptor.Size != 0 { + desc.Size = ia.Descriptor.Size + } + if string(ia.Descriptor.MediaType) != "" { + desc.MediaType = ia.Descriptor.MediaType + } + if ia.Descriptor.Digest != (v1.Hash{}) { + desc.Digest = ia.Descriptor.Digest + } + if ia.Descriptor.Platform != nil { + desc.Platform = ia.Descriptor.Platform + } + if len(ia.Descriptor.URLs) != 0 { + desc.URLs = ia.Descriptor.URLs + } + if len(ia.Descriptor.Annotations) != 0 { + desc.Annotations = ia.Descriptor.Annotations + } + if ia.Descriptor.Data != nil { + desc.Data = ia.Descriptor.Data + } + + return desc, nil +} + +type index struct { + base v1.ImageIndex + adds []IndexAddendum + // remove is removed before adds + remove match.Matcher + + computed bool + manifest *v1.IndexManifest + annotations map[string]string + mediaType *types.MediaType + imageMap map[v1.Hash]v1.Image + indexMap map[v1.Hash]v1.ImageIndex + layerMap map[v1.Hash]v1.Layer + subject *v1.Descriptor +} + +var _ v1.ImageIndex = (*index)(nil) + +func (i *index) MediaType() (types.MediaType, error) { + if i.mediaType != nil { + return *i.mediaType, nil + } + return i.base.MediaType() +} + +func (i *index) Size() (int64, error) { return partial.Size(i) } + +func (i *index) compute() error { + // Don't re-compute if already computed. + if i.computed { + return nil + } + + i.imageMap = make(map[v1.Hash]v1.Image) + i.indexMap = make(map[v1.Hash]v1.ImageIndex) + i.layerMap = make(map[v1.Hash]v1.Layer) + + m, err := i.base.IndexManifest() + if err != nil { + return err + } + manifest := m.DeepCopy() + manifests := manifest.Manifests + + if i.remove != nil { + var cleanedManifests []v1.Descriptor + for _, m := range manifests { + if !i.remove(m) { + cleanedManifests = append(cleanedManifests, m) + } + } + manifests = cleanedManifests + } + + for _, add := range i.adds { + desc, err := computeDescriptor(add) + if err != nil { + return err + } + + manifests = append(manifests, *desc) + if idx, ok := add.Add.(v1.ImageIndex); ok { + i.indexMap[desc.Digest] = idx + } else if img, ok := add.Add.(v1.Image); ok { + i.imageMap[desc.Digest] = img + } else if l, ok := add.Add.(v1.Layer); ok { + i.layerMap[desc.Digest] = l + } else { + logs.Warn.Printf("Unexpected index addendum: %T", add.Add) + } + } + + manifest.Manifests = manifests + + if i.mediaType != nil { + manifest.MediaType = *i.mediaType + } + + if i.annotations != nil { + if manifest.Annotations == nil { + manifest.Annotations = map[string]string{} + } + for k, v := range i.annotations { + manifest.Annotations[k] = v + } + } + manifest.Subject = i.subject + + i.manifest = manifest + i.computed = true + return nil +} + +func (i *index) Image(h v1.Hash) (v1.Image, error) { + if img, ok := i.imageMap[h]; ok { + return img, nil + } + return i.base.Image(h) +} + +func (i *index) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + if idx, ok := i.indexMap[h]; ok { + return idx, nil + } + return i.base.ImageIndex(h) +} + +type withLayer interface { + Layer(v1.Hash) (v1.Layer, error) +} + +// Workaround for #819. +func (i *index) Layer(h v1.Hash) (v1.Layer, error) { + if layer, ok := i.layerMap[h]; ok { + return layer, nil + } + if wl, ok := i.base.(withLayer); ok { + return wl.Layer(h) + } + return nil, fmt.Errorf("layer not found: %s", h) +} + +// Digest returns the sha256 of this image's manifest. +func (i *index) Digest() (v1.Hash, error) { + if err := i.compute(); err != nil { + return v1.Hash{}, err + } + return partial.Digest(i) +} + +// Manifest returns this image's Manifest object. +func (i *index) IndexManifest() (*v1.IndexManifest, error) { + if err := i.compute(); err != nil { + return nil, err + } + return i.manifest.DeepCopy(), nil +} + +// RawManifest returns the serialized bytes of Manifest() +func (i *index) RawManifest() ([]byte, error) { + if err := i.compute(); err != nil { + return nil, err + } + return json.Marshal(i.manifest) +} diff --git a/pkg/v1/mutate/index_test.go b/pkg/v1/mutate/index_test.go new file mode 100644 index 0000000..1f542d1 --- /dev/null +++ b/pkg/v1/mutate/index_test.go @@ -0,0 +1,235 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate_test + +import ( + "log" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestAppendIndex(t *testing.T) { + base, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + idx, err := random.Index(2048, 1, 2) + if err != nil { + t.Fatal(err) + } + img, err := random.Image(4096, 5) + if err != nil { + t.Fatal(err) + } + l, err := random.Layer(1024, types.OCIUncompressedRestrictedLayer) + if err != nil { + t.Fatal(err) + } + + weirdHash := v1.Hash{ + Algorithm: "sha256", + Hex: strings.Repeat("0", 64), + } + + add := mutate.AppendManifests(base, mutate.IndexAddendum{ + Add: idx, + Descriptor: v1.Descriptor{ + URLs: []string{"index.example.com"}, + }, + }, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + URLs: []string{"image.example.com"}, + }, + }, mutate.IndexAddendum{ + Add: l, + Descriptor: v1.Descriptor{ + MediaType: types.MediaType("application/xml"), + URLs: []string{"blob.example.com"}, + }, + }, mutate.IndexAddendum{ + Add: l, + Descriptor: v1.Descriptor{ + URLs: []string{"layer.example.com"}, + Size: 1337, + Digest: weirdHash, + Platform: &v1.Platform{ + OS: "haiku", + Architecture: "toaster", + }, + Annotations: map[string]string{"weird": "true"}, + }, + }) + + if err := validate.Index(add); err != nil { + t.Errorf("Validate() = %v", err) + } + + got, err := add.MediaType() + if err != nil { + t.Fatal(err) + } + want, err := base.MediaType() + if err != nil { + t.Fatal(err) + } + if got != want { + t.Errorf("MediaType() = %s != %s", got, want) + } + + // TODO(jonjohnsonjr): There's no way to grab layers from v1.ImageIndex. + m, err := add.IndexManifest() + if err != nil { + log.Fatal(err) + } + + for i, want := range map[int]string{ + 3: "index.example.com", + 4: "image.example.com", + 5: "blob.example.com", + 6: "layer.example.com", + } { + if got := m.Manifests[i].URLs[0]; got != want { + t.Errorf("wrong URLs[0] for Manifests[%d]: %s != %s", i, got, want) + } + } + + if got, want := m.Manifests[5].MediaType, types.MediaType("application/xml"); got != want { + t.Errorf("wrong MediaType for layer: %s != %s", got, want) + } + + if got, want := m.Manifests[6].MediaType, types.OCIUncompressedRestrictedLayer; got != want { + t.Errorf("wrong MediaType for layer: %s != %s", got, want) + } + + // Append the index to itself and make sure it still validates. + add = mutate.AppendManifests(add, mutate.IndexAddendum{ + Add: add, + }) + if err := validate.Index(add); err != nil { + t.Errorf("Validate() = %v", err) + } + + // Wrap the whole thing in another index and make sure it still validates. + add = mutate.AppendManifests(empty.Index, mutate.IndexAddendum{ + Add: add, + }) + if err := validate.Index(add); err != nil { + t.Errorf("Validate() = %v", err) + } +} + +func TestIndexImmutability(t *testing.T) { + base, err := random.Index(1024, 3, 3) + if err != nil { + t.Fatal(err) + } + ii, err := random.Index(2048, 1, 2) + if err != nil { + t.Fatal(err) + } + i, err := random.Image(4096, 5) + if err != nil { + t.Fatal(err) + } + idx := mutate.AppendManifests(base, mutate.IndexAddendum{ + Add: ii, + Descriptor: v1.Descriptor{ + URLs: []string{"index.example.com"}, + }, + }, mutate.IndexAddendum{ + Add: i, + Descriptor: v1.Descriptor{ + URLs: []string{"image.example.com"}, + }, + }) + + t.Run("index manifest", func(t *testing.T) { + // Check that Manifest is immutable. + changed, err := idx.IndexManifest() + if err != nil { + t.Errorf("IndexManifest() = %v", err) + } + want := changed.DeepCopy() // Create a copy of original before mutating it. + changed.MediaType = types.DockerManifestList + + if got, err := idx.IndexManifest(); err != nil { + t.Errorf("IndexManifest() = %v", err) + } else if !cmp.Equal(got, want) { + t.Errorf("IndexManifest changed! %s", cmp.Diff(got, want)) + } + }) +} + +// TestAppend_ArtifactType tests that appending an image manifest that has a +// non-standard config.mediaType to an index, results in the image's +// config.mediaType being hoisted into the descriptor inside the index, +// as artifactType. +func TestAppend_ArtifactType(t *testing.T) { + for _, c := range []struct { + desc, configMediaType, wantArtifactType string + }{{ + desc: "standard config.mediaType, no artifactType", + configMediaType: string(types.DockerConfigJSON), + wantArtifactType: "", + }, { + desc: "non-standard config.mediaType, want artifactType", + configMediaType: "application/vnd.custom.something", + wantArtifactType: "application/vnd.custom.something", + }} { + t.Run(c.desc, func(t *testing.T) { + img, err := random.Image(1, 1) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + img = mutate.ConfigMediaType(img, types.MediaType(c.configMediaType)) + idx := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{ + Add: img, + }) + mf, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest: %v", err) + } + if got := mf.Manifests[0].ArtifactType; got != c.wantArtifactType { + t.Errorf("manifest artifactType: got %q, want %q", got, c.wantArtifactType) + } + + desc, err := partial.Descriptor(img) + if err != nil { + t.Fatalf("partial.Descriptor: %v", err) + } + if got := desc.ArtifactType; got != c.wantArtifactType { + t.Errorf("descriptor artifactType: got %q, want %q", got, c.wantArtifactType) + } + + gotAT, err := partial.ArtifactType(img) + if err != nil { + t.Fatalf("partial.ArtifactType: %v", err) + } + if gotAT != c.wantArtifactType { + t.Errorf("partial.ArtifactType: got %q, want %q", gotAT, c.wantArtifactType) + } + }) + } +} diff --git a/pkg/v1/mutate/mutate.go b/pkg/v1/mutate/mutate.go new file mode 100644 index 0000000..e4a0e52 --- /dev/null +++ b/pkg/v1/mutate/mutate.go @@ -0,0 +1,553 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "strings" + "time" + + "github.com/google/go-containerregistry/internal/gzip" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +const whiteoutPrefix = ".wh." + +// Addendum contains layers and history to be appended +// to a base image +type Addendum struct { + Layer v1.Layer + History v1.History + URLs []string + Annotations map[string]string + MediaType types.MediaType +} + +// AppendLayers applies layers to a base image. +func AppendLayers(base v1.Image, layers ...v1.Layer) (v1.Image, error) { + additions := make([]Addendum, 0, len(layers)) + for _, layer := range layers { + additions = append(additions, Addendum{Layer: layer}) + } + + return Append(base, additions...) +} + +// Append will apply the list of addendums to the base image +func Append(base v1.Image, adds ...Addendum) (v1.Image, error) { + if len(adds) == 0 { + return base, nil + } + if err := validate(adds); err != nil { + return nil, err + } + + return &image{ + base: base, + adds: adds, + }, nil +} + +// Appendable is an interface that represents something that can be appended +// to an ImageIndex. We need to be able to construct a v1.Descriptor in order +// to append something, and this is the minimum required information for that. +type Appendable interface { + MediaType() (types.MediaType, error) + Digest() (v1.Hash, error) + Size() (int64, error) +} + +// IndexAddendum represents an appendable thing and all the properties that +// we may want to override in the resulting v1.Descriptor. +type IndexAddendum struct { + Add Appendable + v1.Descriptor +} + +// AppendManifests appends a manifest to the ImageIndex. +func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex { + return &index{ + base: base, + adds: adds, + } +} + +// RemoveManifests removes any descriptors that match the match.Matcher. +func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex { + return &index{ + base: base, + remove: matcher, + } +} + +// Config mutates the provided v1.Image to have the provided v1.Config +func Config(base v1.Image, cfg v1.Config) (v1.Image, error) { + cf, err := base.ConfigFile() + if err != nil { + return nil, err + } + + cf.Config = cfg + + return ConfigFile(base, cf) +} + +// Subject mutates the subject on an image or index manifest. +// +// The input is expected to be a v1.Image or v1.ImageIndex, and +// returns the same type. You can type-assert the result like so: +// +// img := Subject(empty.Image, subj).(v1.Image) +// +// Or for an index: +// +// idx := Subject(empty.Index, subj).(v1.ImageIndex) +// +// If the input is not an Image or ImageIndex, the result will +// attempt to lazily annotate the raw manifest. +func Subject(f partial.WithRawManifest, subject v1.Descriptor) partial.WithRawManifest { + if img, ok := f.(v1.Image); ok { + return &image{ + base: img, + subject: &subject, + } + } + if idx, ok := f.(v1.ImageIndex); ok { + return &index{ + base: idx, + subject: &subject, + } + } + return arbitraryRawManifest{a: f, subject: &subject} +} + +// Annotations mutates the annotations on an annotatable image or index manifest. +// +// The annotatable input is expected to be a v1.Image or v1.ImageIndex, and +// returns the same type. You can type-assert the result like so: +// +// img := Annotations(empty.Image, map[string]string{ +// "foo": "bar", +// }).(v1.Image) +// +// Or for an index: +// +// idx := Annotations(empty.Index, map[string]string{ +// "foo": "bar", +// }).(v1.ImageIndex) +// +// If the input Annotatable is not an Image or ImageIndex, the result will +// attempt to lazily annotate the raw manifest. +func Annotations(f partial.WithRawManifest, anns map[string]string) partial.WithRawManifest { + if img, ok := f.(v1.Image); ok { + return &image{ + base: img, + annotations: anns, + } + } + if idx, ok := f.(v1.ImageIndex); ok { + return &index{ + base: idx, + annotations: anns, + } + } + return arbitraryRawManifest{a: f, anns: anns} +} + +type arbitraryRawManifest struct { + a partial.WithRawManifest + anns map[string]string + subject *v1.Descriptor +} + +func (a arbitraryRawManifest) RawManifest() ([]byte, error) { + b, err := a.a.RawManifest() + if err != nil { + return nil, err + } + var m map[string]any + if err := json.Unmarshal(b, &m); err != nil { + return nil, err + } + if ann, ok := m["annotations"]; ok { + if annm, ok := ann.(map[string]string); ok { + for k, v := range a.anns { + annm[k] = v + } + } else { + return nil, fmt.Errorf(".annotations is not a map: %T", ann) + } + } else { + m["annotations"] = a.anns + } + if a.subject != nil { + m["subject"] = a.subject + } + return json.Marshal(m) +} + +// ConfigFile mutates the provided v1.Image to have the provided v1.ConfigFile +func ConfigFile(base v1.Image, cfg *v1.ConfigFile) (v1.Image, error) { + m, err := base.Manifest() + if err != nil { + return nil, err + } + + image := &image{ + base: base, + manifest: m.DeepCopy(), + configFile: cfg, + } + + return image, nil +} + +// CreatedAt mutates the provided v1.Image to have the provided v1.Time +func CreatedAt(base v1.Image, created v1.Time) (v1.Image, error) { + cf, err := base.ConfigFile() + if err != nil { + return nil, err + } + + cfg := cf.DeepCopy() + cfg.Created = created + + return ConfigFile(base, cfg) +} + +// Extract takes an image and returns an io.ReadCloser containing the image's +// flattened filesystem. +// +// Callers can read the filesystem contents by passing the reader to +// tar.NewReader, or io.Copy it directly to some output. +// +// If a caller doesn't read the full contents, they should Close it to free up +// resources used during extraction. +func Extract(img v1.Image) io.ReadCloser { + pr, pw := io.Pipe() + + go func() { + // Close the writer with any errors encountered during + // extraction. These errors will be returned by the reader end + // on subsequent reads. If err == nil, the reader will return + // EOF. + pw.CloseWithError(extract(img, pw)) + }() + + return pr +} + +// Adapted from https://github.com/google/containerregistry/blob/da03b395ccdc4e149e34fbb540483efce962dc64/client/v2_2/docker_image_.py#L816 +func extract(img v1.Image, w io.Writer) error { + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + + fileMap := map[string]bool{} + + layers, err := img.Layers() + if err != nil { + return fmt.Errorf("retrieving image layers: %w", err) + } + + // we iterate through the layers in reverse order because it makes handling + // whiteout layers more efficient, since we can just keep track of the removed + // files as we see .wh. layers and ignore those in previous layers. + for i := len(layers) - 1; i >= 0; i-- { + layer := layers[i] + layerReader, err := layer.Uncompressed() + if err != nil { + return fmt.Errorf("reading layer contents: %w", err) + } + defer layerReader.Close() + tarReader := tar.NewReader(layerReader) + for { + header, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return fmt.Errorf("reading tar: %w", err) + } + + // Some tools prepend everything with "./", so if we don't Clean the + // name, we may have duplicate entries, which angers tar-split. + header.Name = filepath.Clean(header.Name) + // force PAX format to remove Name/Linkname length limit of 100 characters + // required by USTAR and to not depend on internal tar package guess which + // prefers USTAR over PAX + header.Format = tar.FormatPAX + + basename := filepath.Base(header.Name) + dirname := filepath.Dir(header.Name) + tombstone := strings.HasPrefix(basename, whiteoutPrefix) + if tombstone { + basename = basename[len(whiteoutPrefix):] + } + + // check if we have seen value before + // if we're checking a directory, don't filepath.Join names + var name string + if header.Typeflag == tar.TypeDir { + name = header.Name + } else { + name = filepath.Join(dirname, basename) + } + + if _, ok := fileMap[name]; ok { + continue + } + + // check for a whited out parent directory + if inWhiteoutDir(fileMap, name) { + continue + } + + // mark file as handled. non-directory implicitly tombstones + // any entries with a matching (or child) name + fileMap[name] = tombstone || !(header.Typeflag == tar.TypeDir) + if !tombstone { + if err := tarWriter.WriteHeader(header); err != nil { + return err + } + if header.Size > 0 { + if _, err := io.CopyN(tarWriter, tarReader, header.Size); err != nil { + return err + } + } + } + } + } + return nil +} + +func inWhiteoutDir(fileMap map[string]bool, file string) bool { + for { + if file == "" { + break + } + dirname := filepath.Dir(file) + if file == dirname { + break + } + if val, ok := fileMap[dirname]; ok && val { + return true + } + file = dirname + } + return false +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// Time sets all timestamps in an image to the given timestamp. +func Time(img v1.Image, t time.Time) (v1.Image, error) { + newImage := empty.Image + + layers, err := img.Layers() + if err != nil { + return nil, fmt.Errorf("getting image layers: %w", err) + } + + ocf, err := img.ConfigFile() + if err != nil { + return nil, fmt.Errorf("getting original config file: %w", err) + } + + addendums := make([]Addendum, max(len(ocf.History), len(layers))) + var historyIdx, addendumIdx int + for layerIdx := 0; layerIdx < len(layers); addendumIdx, layerIdx = addendumIdx+1, layerIdx+1 { + newLayer, err := layerTime(layers[layerIdx], t) + if err != nil { + return nil, fmt.Errorf("setting layer times: %w", err) + } + + // try to search for the history entry that corresponds to this layer + for ; historyIdx < len(ocf.History); historyIdx++ { + addendums[addendumIdx].History = ocf.History[historyIdx] + // if it's an EmptyLayer, do not set the Layer and have the Addendum with just the History + // and move on to the next History entry + if ocf.History[historyIdx].EmptyLayer { + addendumIdx++ + continue + } + // otherwise, we can exit from the cycle + historyIdx++ + break + } + addendums[addendumIdx].Layer = newLayer + } + + // add all leftover History entries + for ; historyIdx < len(ocf.History); historyIdx, addendumIdx = historyIdx+1, addendumIdx+1 { + addendums[addendumIdx].History = ocf.History[historyIdx] + } + + newImage, err = Append(newImage, addendums...) + if err != nil { + return nil, fmt.Errorf("appending layers: %w", err) + } + + cf, err := newImage.ConfigFile() + if err != nil { + return nil, fmt.Errorf("setting config file: %w", err) + } + + cfg := cf.DeepCopy() + + // Copy basic config over + cfg.Architecture = ocf.Architecture + cfg.OS = ocf.OS + cfg.OSVersion = ocf.OSVersion + cfg.Config = ocf.Config + + // Strip away timestamps from the config file + cfg.Created = v1.Time{Time: t} + + for i, h := range cfg.History { + h.Created = v1.Time{Time: t} + h.CreatedBy = ocf.History[i].CreatedBy + h.Comment = ocf.History[i].Comment + h.EmptyLayer = ocf.History[i].EmptyLayer + // Explicitly ignore Author field; which hinders reproducibility + h.Author = "" + cfg.History[i] = h + } + + return ConfigFile(newImage, cfg) +} + +func layerTime(layer v1.Layer, t time.Time) (v1.Layer, error) { + layerReader, err := layer.Uncompressed() + if err != nil { + return nil, fmt.Errorf("getting layer: %w", err) + } + defer layerReader.Close() + w := new(bytes.Buffer) + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + + tarReader := tar.NewReader(layerReader) + for { + header, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, fmt.Errorf("reading layer: %w", err) + } + + header.ModTime = t + + //PAX and GNU Format support additional timestamps in the header + if header.Format == tar.FormatPAX || header.Format == tar.FormatGNU { + header.AccessTime = t + header.ChangeTime = t + } + + if err := tarWriter.WriteHeader(header); err != nil { + return nil, fmt.Errorf("writing tar header: %w", err) + } + + if header.Typeflag == tar.TypeReg { + // TODO(#1168): This should be lazy, and not buffer the entire layer contents. + if _, err = io.CopyN(tarWriter, tarReader, header.Size); err != nil { + return nil, fmt.Errorf("writing layer file: %w", err) + } + } + } + + if err := tarWriter.Close(); err != nil { + return nil, err + } + + b := w.Bytes() + // gzip the contents, then create the layer + opener := func() (io.ReadCloser, error) { + return gzip.ReadCloser(io.NopCloser(bytes.NewReader(b))), nil + } + layer, err = tarball.LayerFromOpener(opener) + if err != nil { + return nil, fmt.Errorf("creating layer: %w", err) + } + + return layer, nil +} + +// Canonical is a helper function to combine Time and configFile +// to remove any randomness during a docker build. +func Canonical(img v1.Image) (v1.Image, error) { + // Set all timestamps to 0 + created := time.Time{} + img, err := Time(img, created) + if err != nil { + return nil, err + } + + cf, err := img.ConfigFile() + if err != nil { + return nil, err + } + + // Get rid of host-dependent random config + cfg := cf.DeepCopy() + + cfg.Container = "" + cfg.Config.Hostname = "" + cfg.DockerVersion = "" + + return ConfigFile(img, cfg) +} + +// MediaType modifies the MediaType() of the given image. +func MediaType(img v1.Image, mt types.MediaType) v1.Image { + return &image{ + base: img, + mediaType: &mt, + } +} + +// ConfigMediaType modifies the MediaType() of the given image's Config. +// +// If !mt.IsConfig(), this will be the image's artifactType in any indexes it's a part of. +func ConfigMediaType(img v1.Image, mt types.MediaType) v1.Image { + return &image{ + base: img, + configMediaType: &mt, + } +} + +// IndexMediaType modifies the MediaType() of the given index. +func IndexMediaType(idx v1.ImageIndex, mt types.MediaType) v1.ImageIndex { + return &index{ + base: idx, + mediaType: &mt, + } +} diff --git a/pkg/v1/mutate/mutate_test.go b/pkg/v1/mutate/mutate_test.go new file mode 100644 index 0000000..c4fdba6 --- /dev/null +++ b/pkg/v1/mutate/mutate_test.go @@ -0,0 +1,770 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate_test + +import ( + "archive/tar" + "bytes" + "errors" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/match" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestExtractWhiteout(t *testing.T) { + img, err := tarball.ImageFromPath("testdata/whiteout_image.tar", nil) + if err != nil { + t.Errorf("Error loading image: %v", err) + } + tarPath, _ := filepath.Abs("img.tar") + defer os.Remove(tarPath) + tr := tar.NewReader(mutate.Extract(img)) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + name := header.Name + for _, part := range filepath.SplitList(name) { + if part == "foo" { + t.Errorf("whiteout file found in tar: %v", name) + } + } + } +} + +func TestExtractOverwrittenFile(t *testing.T) { + img, err := tarball.ImageFromPath("testdata/overwritten_file.tar", nil) + if err != nil { + t.Fatalf("Error loading image: %v", err) + } + tr := tar.NewReader(mutate.Extract(img)) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + name := header.Name + if strings.Contains(name, "foo.txt") { + var buf bytes.Buffer + buf.ReadFrom(tr) + if strings.Contains(buf.String(), "foo") { + t.Errorf("Contents of file were not correctly overwritten") + } + } + } +} + +// TestExtractError tests that if there are any errors encountered +func TestExtractError(t *testing.T) { + rc := mutate.Extract(invalidImage{}) + if _, err := io.Copy(io.Discard, rc); err == nil { + t.Errorf("rc.Read; got nil error") + } else if !strings.Contains(err.Error(), errInvalidImage.Error()) { + t.Errorf("rc.Read; got %v, want %v", err, errInvalidImage) + } +} + +// TestExtractPartialRead tests that the reader can be partially read (e.g., +// tar headers) and closed without error. +func TestExtractPartialRead(t *testing.T) { + rc := mutate.Extract(invalidImage{}) + if _, err := io.Copy(io.Discard, io.LimitReader(rc, 1)); err != nil { + t.Errorf("Could not read one byte from reader") + } + if err := rc.Close(); err != nil { + t.Errorf("rc.Close: %v", err) + } +} + +// invalidImage is an image which returns an error when Layers() is called. +type invalidImage struct { + v1.Image +} + +var errInvalidImage = errors.New("invalid image") + +func (invalidImage) Layers() ([]v1.Layer, error) { + return nil, errInvalidImage +} + +func TestNoopCondition(t *testing.T) { + source := sourceImage(t) + + result, err := mutate.AppendLayers(source, []v1.Layer{}...) + if err != nil { + t.Fatalf("Unexpected error creating a writable image: %v", err) + } + + if !manifestsAreEqual(t, source, result) { + t.Error("manifests are not the same") + } + + if !configFilesAreEqual(t, source, result) { + t.Fatal("config files are not the same") + } +} + +func TestAppendWithAddendum(t *testing.T) { + source := sourceImage(t) + + addendum := mutate.Addendum{ + Layer: mockLayer{}, + History: v1.History{ + Author: "dave", + }, + URLs: []string{ + "example.com", + }, + Annotations: map[string]string{ + "foo": "bar", + }, + MediaType: types.MediaType("foo"), + } + + result, err := mutate.Append(source, addendum) + if err != nil { + t.Fatalf("failed to append: %v", err) + } + + layers := getLayers(t, result) + + if diff := cmp.Diff(layers[1], mockLayer{}); diff != "" { + t.Fatalf("correct layer was not appended (-got, +want) %v", diff) + } + + if configSizesAreEqual(t, source, result) { + t.Fatal("adding a layer MUST change the config file size") + } + + cf := getConfigFile(t, result) + + if diff := cmp.Diff(cf.History[1], addendum.History); diff != "" { + t.Fatalf("the appended history is not the same (-got, +want) %s", diff) + } + + m, err := result.Manifest() + if err != nil { + t.Fatalf("failed to get manifest: %v", err) + } + + if diff := cmp.Diff(m.Layers[1].URLs, addendum.URLs); diff != "" { + t.Fatalf("the appended URLs is not the same (-got, +want) %s", diff) + } + + if diff := cmp.Diff(m.Layers[1].Annotations, addendum.Annotations); diff != "" { + t.Fatalf("the appended Annotations is not the same (-got, +want) %s", diff) + } + if diff := cmp.Diff(m.Layers[1].MediaType, addendum.MediaType); diff != "" { + t.Fatalf("the appended MediaType is not the same (-got, +want) %s", diff) + } +} + +func TestAppendLayers(t *testing.T) { + source := sourceImage(t) + layer, err := random.Layer(100, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + result, err := mutate.AppendLayers(source, layer) + if err != nil { + t.Fatalf("failed to append a layer: %v", err) + } + + if manifestsAreEqual(t, source, result) { + t.Fatal("appending a layer did not mutate the manifest") + } + + if configFilesAreEqual(t, source, result) { + t.Fatal("appending a layer did not mutate the config file") + } + + if configSizesAreEqual(t, source, result) { + t.Fatal("adding a layer MUST change the config file size") + } + + layers := getLayers(t, result) + + if got, want := len(layers), 2; got != want { + t.Fatalf("Layers did not return the appended layer "+ + "- got size %d; expected 2", len(layers)) + } + + if layers[1] != layer { + t.Errorf("correct layer was not appended: got %v; want %v", layers[1], layer) + } + + if err := validate.Image(result); err != nil { + t.Errorf("validate.Image() = %v", err) + } +} + +func TestMutateConfig(t *testing.T) { + source := sourceImage(t) + cfg, err := source.ConfigFile() + if err != nil { + t.Fatalf("error getting source config file") + } + + newEnv := []string{"foo=bar"} + cfg.Config.Env = newEnv + result, err := mutate.Config(source, cfg.Config) + if err != nil { + t.Fatalf("failed to mutate a config: %v", err) + } + + if manifestsAreEqual(t, source, result) { + t.Error("mutating the config MUST mutate the manifest") + } + + if configFilesAreEqual(t, source, result) { + t.Error("mutating the config did not mutate the config file") + } + + if configSizesAreEqual(t, source, result) { + t.Error("adding an environment variable MUST change the config file size") + } + + if configDigestsAreEqual(t, source, result) { + t.Errorf("mutating the config MUST mutate the config digest") + } + + if !reflect.DeepEqual(cfg.Config.Env, newEnv) { + t.Errorf("incorrect environment set %v!=%v", cfg.Config.Env, newEnv) + } + + if err := validate.Image(result); err != nil { + t.Errorf("validate.Image() = %v", err) + } +} + +type arbitrary struct { +} + +func (arbitrary) RawManifest() ([]byte, error) { + return []byte(`{"hello":"world"}`), nil +} +func TestAnnotations(t *testing.T) { + anns := map[string]string{ + "foo": "bar", + } + + for _, c := range []struct { + desc string + in partial.WithRawManifest + want string + }{{ + desc: "image", + in: empty.Image, + want: `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":115,"digest":"sha256:5b943e2b943f6c81dbbd4e2eca5121f4fcc39139e3d1219d6d89bd925b77d9fe"},"layers":[],"annotations":{"foo":"bar"}}`, + }, { + desc: "index", + in: empty.Index, + want: `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":null,"annotations":{"foo":"bar"}}`, + }, { + desc: "arbitrary", + in: arbitrary{}, + want: `{"annotations":{"foo":"bar"},"hello":"world"}`, + }} { + t.Run(c.desc, func(t *testing.T) { + got, err := mutate.Annotations(c.in, anns).RawManifest() + if err != nil { + t.Fatalf("Annotations: %v", err) + } + if d := cmp.Diff(c.want, string(got)); d != "" { + t.Errorf("Diff(-want,+got): %s", d) + } + }) + } +} + +func TestMutateCreatedAt(t *testing.T) { + source := sourceImage(t) + want := time.Now().Add(-2 * time.Minute) + result, err := mutate.CreatedAt(source, v1.Time{Time: want}) + if err != nil { + t.Fatalf("CreatedAt: %v", err) + } + + if configDigestsAreEqual(t, source, result) { + t.Errorf("mutating the created time MUST mutate the config digest") + } + + got := getConfigFile(t, result).Created.Time + if got != want { + t.Errorf("mutating the created time MUST mutate the time from %v to %v", got, want) + } +} + +func TestMutateTime(t *testing.T) { + for _, tc := range []struct { + name string + source v1.Image + }{ + { + name: "image with matching history and layers", + source: sourceImage(t), + }, + { + name: "image with empty_layer history entries", + source: sourceImagePath(t, "testdata/source_image_with_empty_layer_history.tar"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + want := time.Time{} + result, err := mutate.Time(tc.source, want) + if err != nil { + t.Fatalf("failed to mutate a config: %v", err) + } + + if configDigestsAreEqual(t, tc.source, result) { + t.Fatal("mutating the created time MUST mutate the config digest") + } + + mutatedOriginalConfig := getConfigFile(t, tc.source).DeepCopy() + gotConfig := getConfigFile(t, result) + + // manually change the fields we expect to be changed by mutate.Time + mutatedOriginalConfig.Author = "" + mutatedOriginalConfig.Created = v1.Time{Time: want} + for i := range mutatedOriginalConfig.History { + mutatedOriginalConfig.History[i].Created = v1.Time{Time: want} + mutatedOriginalConfig.History[i].Author = "" + } + + if diff := cmp.Diff(mutatedOriginalConfig, gotConfig, + cmpopts.IgnoreFields(v1.RootFS{}, "DiffIDs"), + ); diff != "" { + t.Errorf("configFile() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestMutateMediaType(t *testing.T) { + want := types.OCIManifestSchema1 + wantCfg := types.OCIConfigJSON + img := mutate.MediaType(empty.Image, want) + img = mutate.ConfigMediaType(img, wantCfg) + got, err := img.MediaType() + if err != nil { + t.Fatal(err) + } + if want != got { + t.Errorf("%q != %q", want, got) + } + manifest, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + if manifest.MediaType == "" { + t.Error("MediaType should be set for OCI media types") + } + if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg { + t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg) + } + + want = types.DockerManifestSchema2 + wantCfg = types.DockerConfigJSON + img = mutate.MediaType(img, want) + img = mutate.ConfigMediaType(img, wantCfg) + got, err = img.MediaType() + if err != nil { + t.Fatal(err) + } + if want != got { + t.Errorf("%q != %q", want, got) + } + manifest, err = img.Manifest() + if err != nil { + t.Fatal(err) + } + if manifest.MediaType != want { + t.Errorf("MediaType should be set for Docker media types: %v", manifest.MediaType) + } + if gotCfg := manifest.Config.MediaType; gotCfg != wantCfg { + t.Errorf("manifest.Config.MediaType = %v, wanted %v", gotCfg, wantCfg) + } + + want = types.OCIImageIndex + idx := mutate.IndexMediaType(empty.Index, want) + got, err = idx.MediaType() + if err != nil { + t.Fatal(err) + } + if want != got { + t.Errorf("%q != %q", want, got) + } + im, err := idx.IndexManifest() + if err != nil { + t.Fatal(err) + } + if im.MediaType == "" { + t.Error("MediaType should be set for OCI media types") + } + + want = types.DockerManifestList + idx = mutate.IndexMediaType(idx, want) + got, err = idx.MediaType() + if err != nil { + t.Fatal(err) + } + if want != got { + t.Errorf("%q != %q", want, got) + } + im, err = idx.IndexManifest() + if err != nil { + t.Fatal(err) + } + if im.MediaType != want { + t.Errorf("MediaType should be set for Docker media types: %v", im.MediaType) + } +} + +func TestAppendStreamableLayer(t *testing.T) { + img, err := mutate.AppendLayers( + sourceImage(t), + stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("a", 100)))), + stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("b", 100)))), + stream.NewLayer(io.NopCloser(strings.NewReader(strings.Repeat("c", 100)))), + ) + if err != nil { + t.Fatalf("AppendLayers: %v", err) + } + + // Until the streams are consumed, the image manifest is not yet computed. + if _, err := img.Manifest(); !errors.Is(err, stream.ErrNotComputed) { + t.Errorf("Manifest: got %v, want %v", err, stream.ErrNotComputed) + } + + // We can still get Layers while some are not yet computed. + ls, err := img.Layers() + if err != nil { + t.Errorf("Layers: %v", err) + } + wantDigests := []string{ + "sha256:bfa1c600931132f55789459e2f5a5eb85659ac91bc5a54ce09e3ed14809f8a7f", + "sha256:77a52b9a141dcc4d3d277d053193765dca725626f50eaf56b903ac2439cf7fd1", + "sha256:b78472d63f6e3d31059819173b56fcb0d9479a2b13c097d4addd84889f6aff06", + } + for i, l := range ls[1:] { + rc, err := l.Compressed() + if err != nil { + t.Errorf("Layer %d Compressed: %v", i, err) + } + + // Consume the layer's stream and close it to compute the + // layer's metadata. + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Errorf("Reading layer %d: %v", i, err) + } + if err := rc.Close(); err != nil { + t.Errorf("Closing layer %d: %v", i, err) + } + + // The layer's metadata is now available. + h, err := l.Digest() + if err != nil { + t.Errorf("Digest after consuming layer %d: %v", i, err) + } + if h.String() != wantDigests[i] { + t.Errorf("Layer %d digest got %q, want %q", i, h, wantDigests[i]) + } + } + + // Now that the streamable layers have been consumed, the image's + // manifest can be computed. + if _, err := img.Manifest(); err != nil { + t.Errorf("Manifest: %v", err) + } + + h, err := img.Digest() + if err != nil { + t.Errorf("Digest: %v", err) + } + wantDigest := "sha256:14d140947afedc6901b490265a08bc8ebe7f9d9faed6fdf19a451f054a7dd746" + if h.String() != wantDigest { + t.Errorf("Image digest got %q, want %q", h, wantDigest) + } +} + +func TestCanonical(t *testing.T) { + source := sourceImage(t) + img, err := mutate.Canonical(source) + if err != nil { + t.Fatal(err) + } + sourceCf, err := source.ConfigFile() + if err != nil { + t.Fatal(err) + } + cf, err := img.ConfigFile() + if err != nil { + t.Fatal(err) + } + for _, h := range cf.History { + want := "bazel build ..." + got := h.CreatedBy + if want != got { + t.Errorf("%q != %q", want, got) + } + } + var want, got string + want = cf.Architecture + got = sourceCf.Architecture + if want != got { + t.Errorf("%q != %q", want, got) + } + want = cf.OS + got = sourceCf.OS + if want != got { + t.Errorf("%q != %q", want, got) + } + want = cf.OSVersion + got = sourceCf.OSVersion + if want != got { + t.Errorf("%q != %q", want, got) + } + for _, s := range []string{ + cf.Container, + cf.Config.Hostname, + cf.DockerVersion, + } { + if s != "" { + t.Errorf("non-zeroed string: %v", s) + } + } + + expectedLayerTime := time.Unix(0, 0) + layers := getLayers(t, img) + for _, layer := range layers { + assertMTime(t, layer, expectedLayerTime) + } +} + +func TestRemoveManifests(t *testing.T) { + // Load up the registry. + count := 3 + for i := 0; i < count; i++ { + ii, err := random.Index(1024, int64(count), int64(count)) + if err != nil { + t.Fatal(err) + } + // test removing the first layer, second layer or the third layer + manifest, err := ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != count { + t.Fatalf("mismatched manifests on setup, had %d, expected %d", len(manifest.Manifests), count) + } + digest := manifest.Manifests[i].Digest + ii = mutate.RemoveManifests(ii, match.Digests(digest)) + manifest, err = ii.IndexManifest() + if err != nil { + t.Fatal(err) + } + if len(manifest.Manifests) != (count - 1) { + t.Fatalf("mismatched manifests after removal, had %d, expected %d", len(manifest.Manifests), count-1) + } + for j, m := range manifest.Manifests { + if m.Digest == digest { + t.Fatalf("unexpectedly found removed hash %v at position %d", digest, j) + } + } + } +} + +func TestImageImmutability(t *testing.T) { + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + + t.Run("manifest", func(t *testing.T) { + // Check that Manifest is immutable. + changed, err := img.Manifest() + if err != nil { + t.Errorf("Manifest() = %v", err) + } + want := changed.DeepCopy() // Create a copy of original before mutating it. + changed.MediaType = types.DockerManifestList + + if got, err := img.Manifest(); err != nil { + t.Errorf("Manifest() = %v", err) + } else if !cmp.Equal(got, want) { + t.Errorf("manifest changed! %s", cmp.Diff(got, want)) + } + }) + + t.Run("config file", func(t *testing.T) { + // Check that ConfigFile is immutable. + changed, err := img.ConfigFile() + if err != nil { + t.Errorf("ConfigFile() = %v", err) + } + want := changed.DeepCopy() // Create a copy of original before mutating it. + changed.Author = "Jay Pegg" + + if got, err := img.ConfigFile(); err != nil { + t.Errorf("ConfigFile() = %v", err) + } else if !cmp.Equal(got, want) { + t.Errorf("ConfigFile changed! %s", cmp.Diff(got, want)) + } + }) +} + +func assertMTime(t *testing.T, layer v1.Layer, expectedTime time.Time) { + l, err := layer.Uncompressed() + + if err != nil { + t.Fatalf("reading layer failed: %v", err) + } + + tr := tar.NewReader(l) + for { + header, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("Error reading layer: %v", err) + } + + mtime := header.ModTime + if mtime.Equal(expectedTime) == false { + t.Errorf("unexpected mod time for layer. expected %v, got %v.", expectedTime, mtime) + } + } +} + +func sourceImage(t *testing.T) v1.Image { + return sourceImagePath(t, "testdata/source_image.tar") +} + +func sourceImagePath(t *testing.T, tarPath string) v1.Image { + t.Helper() + + image, err := tarball.ImageFromPath(tarPath, nil) + if err != nil { + t.Fatalf("Error loading image: %v", err) + } + return image +} + +func getManifest(t *testing.T, i v1.Image) *v1.Manifest { + t.Helper() + + m, err := i.Manifest() + if err != nil { + t.Fatalf("Error fetching image manifest: %v", err) + } + + return m +} + +func getLayers(t *testing.T, i v1.Image) []v1.Layer { + t.Helper() + + l, err := i.Layers() + if err != nil { + t.Fatalf("Error fetching image layers: %v", err) + } + + return l +} + +func getConfigFile(t *testing.T, i v1.Image) *v1.ConfigFile { + t.Helper() + + c, err := i.ConfigFile() + if err != nil { + t.Fatalf("Error fetching image config file: %v", err) + } + + return c +} + +func configFilesAreEqual(t *testing.T, first, second v1.Image) bool { + t.Helper() + + fc := getConfigFile(t, first) + sc := getConfigFile(t, second) + + return cmp.Equal(fc, sc) +} + +func configDigestsAreEqual(t *testing.T, first, second v1.Image) bool { + t.Helper() + + fm := getManifest(t, first) + sm := getManifest(t, second) + + return fm.Config.Digest == sm.Config.Digest +} + +func configSizesAreEqual(t *testing.T, first, second v1.Image) bool { + t.Helper() + + fm := getManifest(t, first) + sm := getManifest(t, second) + + return fm.Config.Size == sm.Config.Size +} + +func manifestsAreEqual(t *testing.T, first, second v1.Image) bool { + t.Helper() + + fm := getManifest(t, first) + sm := getManifest(t, second) + + return cmp.Equal(fm, sm) +} + +type mockLayer struct{} + +func (m mockLayer) Digest() (v1.Hash, error) { + return v1.Hash{Algorithm: "fake", Hex: "digest"}, nil +} + +func (m mockLayer) DiffID() (v1.Hash, error) { + return v1.Hash{Algorithm: "fake", Hex: "diff id"}, nil +} + +func (m mockLayer) MediaType() (types.MediaType, error) { + return "some-media-type", nil +} + +func (m mockLayer) Size() (int64, error) { return 137438691328, nil } +func (m mockLayer) Compressed() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("compressed times")), nil +} +func (m mockLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("uncompressed")), nil +} diff --git a/pkg/v1/mutate/rebase.go b/pkg/v1/mutate/rebase.go new file mode 100644 index 0000000..c606e0b --- /dev/null +++ b/pkg/v1/mutate/rebase.go @@ -0,0 +1,144 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate + +import ( + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" +) + +// Rebase returns a new v1.Image where the oldBase in orig is replaced by newBase. +func Rebase(orig, oldBase, newBase v1.Image) (v1.Image, error) { + // Verify that oldBase's layers are present in orig, otherwise orig is + // not based on oldBase at all. + origLayers, err := orig.Layers() + if err != nil { + return nil, fmt.Errorf("failed to get layers for original: %w", err) + } + oldBaseLayers, err := oldBase.Layers() + if err != nil { + return nil, err + } + if len(oldBaseLayers) > len(origLayers) { + return nil, fmt.Errorf("image %q is not based on %q (too few layers)", orig, oldBase) + } + for i, l := range oldBaseLayers { + oldLayerDigest, err := l.Digest() + if err != nil { + return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, oldBase, err) + } + origLayerDigest, err := origLayers[i].Digest() + if err != nil { + return nil, fmt.Errorf("failed to get digest of layer %d of %q: %w", i, orig, err) + } + if oldLayerDigest != origLayerDigest { + return nil, fmt.Errorf("image %q is not based on %q (layer %d mismatch)", orig, oldBase, i) + } + } + + oldConfig, err := oldBase.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get config for old base: %w", err) + } + + origConfig, err := orig.ConfigFile() + if err != nil { + return nil, fmt.Errorf("failed to get config for original: %w", err) + } + + newConfig, err := newBase.ConfigFile() + if err != nil { + return nil, fmt.Errorf("could not get config for new base: %w", err) + } + + // Stitch together an image that contains: + // - original image's config + // - new base image's os/arch properties + // - new base image's layers + top of original image's layers + // - new base image's history + top of original image's history + rebasedImage, err := Config(empty.Image, *origConfig.Config.DeepCopy()) + if err != nil { + return nil, fmt.Errorf("failed to create empty image with original config: %w", err) + } + + // Add new config properties from existing images. + rebasedConfig, err := rebasedImage.ConfigFile() + if err != nil { + return nil, fmt.Errorf("could not get config for rebased image: %w", err) + } + // OS/Arch properties from new base + rebasedConfig.Architecture = newConfig.Architecture + rebasedConfig.OS = newConfig.OS + rebasedConfig.OSVersion = newConfig.OSVersion + + // Apply config properties to rebased. + rebasedImage, err = ConfigFile(rebasedImage, rebasedConfig) + if err != nil { + return nil, fmt.Errorf("failed to replace config for rebased image: %w", err) + } + + // Get new base layers and config for history. + newBaseLayers, err := newBase.Layers() + if err != nil { + return nil, fmt.Errorf("could not get new base layers for new base: %w", err) + } + // Add new base layers. + rebasedImage, err = Append(rebasedImage, createAddendums(0, 0, newConfig.History, newBaseLayers)...) + if err != nil { + return nil, fmt.Errorf("failed to append new base image: %w", err) + } + + // Add original layers above the old base. + rebasedImage, err = Append(rebasedImage, createAddendums(len(oldConfig.History), len(oldBaseLayers)+1, origConfig.History, origLayers)...) + if err != nil { + return nil, fmt.Errorf("failed to append original image: %w", err) + } + + return rebasedImage, nil +} + +// createAddendums makes a list of addendums from a history and layers starting from a specific history and layer +// indexes. +func createAddendums(startHistory, startLayer int, history []v1.History, layers []v1.Layer) []Addendum { + var adds []Addendum + // History should be a superset of layers; empty layers (e.g. ENV statements) only exist in history. + // They cannot be iterated identically but must be walked independently, only advancing the iterator for layers + // when a history entry for a non-empty layer is seen. + layerIndex := 0 + for historyIndex := range history { + var layer v1.Layer + emptyLayer := history[historyIndex].EmptyLayer + if !emptyLayer { + layer = layers[layerIndex] + layerIndex++ + } + if historyIndex >= startHistory || layerIndex >= startLayer { + adds = append(adds, Addendum{ + Layer: layer, + History: history[historyIndex], + }) + } + } + // In the event history was malformed or non-existent, append the remaining layers. + for i := layerIndex; i < len(layers); i++ { + if i >= startLayer { + adds = append(adds, Addendum{Layer: layers[layerIndex]}) + } + } + + return adds +} diff --git a/pkg/v1/mutate/rebase_test.go b/pkg/v1/mutate/rebase_test.go new file mode 100644 index 0000000..250b6bf --- /dev/null +++ b/pkg/v1/mutate/rebase_test.go @@ -0,0 +1,179 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate_test + +import ( + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" +) + +func layerDigests(t *testing.T, img v1.Image) []string { + layers, err := img.Layers() + if err != nil { + t.Fatalf("oldBase.Layers: %v", err) + } + layerDigests := make([]string, len(layers)) + for i, l := range layers { + dig, err := l.Digest() + if err != nil { + t.Fatalf("layer.Digest %d: %v", i, err) + } + t.Log(dig) + layerDigests[i] = dig.String() + } + return layerDigests +} + +// TestRebase tests that layer digests are expected when performing a rebase on +// random.Image layers. +func TestRebase(t *testing.T) { + // Create a random old base image of 5 layers and get those layers' digests. + const oldBaseLayerCount = 5 + oldBase, err := random.Image(100, oldBaseLayerCount) + if err != nil { + t.Fatalf("random.Image (oldBase): %v", err) + } + t.Log("Old base:") + _ = layerDigests(t, oldBase) + + // Construct an image with 2 layers on top of oldBase (an empty layer and a random layer). + top, err := random.Image(100, 1) + if err != nil { + t.Fatalf("random.Image (top): %v", err) + } + topLayers, err := top.Layers() + if err != nil { + t.Fatalf("top.Layers: %v", err) + } + orig, err := mutate.Append(oldBase, + mutate.Addendum{ + Layer: nil, + History: v1.History{ + Author: "me", + Created: v1.Time{Time: time.Now()}, + CreatedBy: "test-empty", + Comment: "this is an empty test", + EmptyLayer: true, + }, + }, + mutate.Addendum{ + Layer: topLayers[0], + History: v1.History{ + Author: "me", + Created: v1.Time{Time: time.Now()}, + CreatedBy: "test", + Comment: "this is a test", + }, + }, + ) + if err != nil { + t.Fatalf("Append: %v", err) + } + + t.Log("Original:") + origLayerDigests := layerDigests(t, orig) + + // Create a random new base image of 3 layers. + newBase, err := random.Image(100, 3) + if err != nil { + t.Fatalf("random.Image (newBase): %v", err) + } + t.Log("New base:") + newBaseLayerDigests := layerDigests(t, newBase) + + // Add config file os/arch property fields + newBaseConfigFile, err := newBase.ConfigFile() + if err != nil { + t.Fatalf("newBase.ConfigFile: %v", err) + } + newBaseConfigFile.Architecture = "arm" + newBaseConfigFile.OS = "windows" + newBaseConfigFile.OSVersion = "10.0.17763.1339" + + newBase, err = mutate.ConfigFile(newBase, newBaseConfigFile) + if err != nil { + t.Fatalf("ConfigFile (newBase): %v", err) + } + + // Rebase original image onto new base. + rebased, err := mutate.Rebase(orig, oldBase, newBase) + if err != nil { + t.Fatalf("Rebase: %v", err) + } + + rebasedBaseLayers, err := rebased.Layers() + if err != nil { + t.Fatalf("rebased.Layers: %v", err) + } + rebasedLayerDigests := make([]string, len(rebasedBaseLayers)) + t.Log("Rebased image layer digests:") + for i, l := range rebasedBaseLayers { + dig, err := l.Digest() + if err != nil { + t.Fatalf("layer.Digest (rebased base layer %d): %v", i, err) + } + t.Log(dig) + rebasedLayerDigests[i] = dig.String() + } + + // Compare rebased layers. + wantLayerDigests := append(newBaseLayerDigests, origLayerDigests[len(origLayerDigests)-1]) + if len(rebasedLayerDigests) != len(wantLayerDigests) { + t.Fatalf("Rebased image contained %d layers, want %d", len(rebasedLayerDigests), len(wantLayerDigests)) + } + for i, rl := range rebasedLayerDigests { + if got, want := rl, wantLayerDigests[i]; got != want { + t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want) + } + } + + // Compare rebased history. + origConfig, err := orig.ConfigFile() + if err != nil { + t.Fatalf("orig.ConfigFile: %v", err) + } + newBaseConfig, err := newBase.ConfigFile() + if err != nil { + t.Fatalf("newBase.ConfigFile: %v", err) + } + rebasedConfig, err := rebased.ConfigFile() + if err != nil { + t.Fatalf("rebased.ConfigFile: %v", err) + } + wantHistories := append(newBaseConfig.History, origConfig.History[oldBaseLayerCount:]...) + if len(wantHistories) != len(rebasedConfig.History) { + t.Fatalf("Rebased image contained %d history, want %d", len(rebasedConfig.History), len(wantHistories)) + } + for i, rh := range rebasedConfig.History { + if got, want := rh.Comment, wantHistories[i].Comment; got != want { + t.Errorf("Layer %d mismatch, got %q, want %q", i, got, want) + } + } + + // Compare ConfigFile property fields copied from new base. + if rebasedConfig.Architecture != newBaseConfig.Architecture { + t.Errorf("ConfigFile property Architecture mismatch, got %q, want %q", rebasedConfig.Architecture, newBaseConfig.Architecture) + } + if rebasedConfig.OS != newBaseConfig.OS { + t.Errorf("ConfigFile property OS mismatch, got %q, want %q", rebasedConfig.OS, newBaseConfig.OS) + } + if rebasedConfig.OSVersion != newBaseConfig.OSVersion { + t.Errorf("ConfigFile property OSVersion mismatch, got %q, want %q", rebasedConfig.OSVersion, newBaseConfig.OSVersion) + } +} diff --git a/pkg/v1/mutate/testdata/README.md b/pkg/v1/mutate/testdata/README.md new file mode 100644 index 0000000..a35d433 --- /dev/null +++ b/pkg/v1/mutate/testdata/README.md @@ -0,0 +1,10 @@ +# whiteout\_image.tar + +Including whiteout files in our source caused [issues](https://github.com/google/go-containerregistry/issues/305) +when cloning this repo inside a docker build. Removing the whiteout file from +this test data doesn't break anything (since we checked in the tar), but if you +want to rebuild it for some reason: + +``` +touch whiteout/.wh.foo.txt +``` diff --git a/pkg/v1/mutate/testdata/bar b/pkg/v1/mutate/testdata/bar new file mode 100644 index 0000000..5716ca5 --- /dev/null +++ b/pkg/v1/mutate/testdata/bar @@ -0,0 +1 @@ +bar diff --git a/pkg/v1/mutate/testdata/foo b/pkg/v1/mutate/testdata/foo new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/pkg/v1/mutate/testdata/foo @@ -0,0 +1 @@ +foo diff --git a/pkg/v1/mutate/testdata/overwritten_file.tar b/pkg/v1/mutate/testdata/overwritten_file.tar Binary files differnew file mode 100755 index 0000000..7159556 --- /dev/null +++ b/pkg/v1/mutate/testdata/overwritten_file.tar diff --git a/pkg/v1/mutate/testdata/source_image.tar b/pkg/v1/mutate/testdata/source_image.tar Binary files differnew file mode 100755 index 0000000..7824a7b --- /dev/null +++ b/pkg/v1/mutate/testdata/source_image.tar diff --git a/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar b/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar Binary files differnew file mode 100755 index 0000000..541cb37 --- /dev/null +++ b/pkg/v1/mutate/testdata/source_image_with_empty_layer_history.tar diff --git a/pkg/v1/mutate/testdata/whiteout/bar.txt b/pkg/v1/mutate/testdata/whiteout/bar.txt new file mode 100644 index 0000000..5716ca5 --- /dev/null +++ b/pkg/v1/mutate/testdata/whiteout/bar.txt @@ -0,0 +1 @@ +bar diff --git a/pkg/v1/mutate/testdata/whiteout/foo.txt b/pkg/v1/mutate/testdata/whiteout/foo.txt new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/pkg/v1/mutate/testdata/whiteout/foo.txt @@ -0,0 +1 @@ +foo diff --git a/pkg/v1/mutate/testdata/whiteout_image.tar b/pkg/v1/mutate/testdata/whiteout_image.tar Binary files differnew file mode 100755 index 0000000..748621e --- /dev/null +++ b/pkg/v1/mutate/testdata/whiteout_image.tar diff --git a/pkg/v1/mutate/whiteout_test.go b/pkg/v1/mutate/whiteout_test.go new file mode 100644 index 0000000..d3e7a86 --- /dev/null +++ b/pkg/v1/mutate/whiteout_test.go @@ -0,0 +1,43 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mutate + +import ( + "testing" +) + +func TestWhiteoutDir(t *testing.T) { + fsMap := map[string]bool{ + "baz": true, + "red/blue": true, + } + var tests = []struct { + path string + whiteout bool + }{ + {"usr/bin", false}, + {"baz/foo.txt", true}, + {"baz/bar/foo.txt", true}, + {"red/green", false}, + {"red/yellow.txt", false}, + } + + for _, tt := range tests { + whiteout := inWhiteoutDir(fsMap, tt.path) + if whiteout != tt.whiteout { + t.Errorf("Whiteout %s: expected %v, but got %v", tt.path, tt.whiteout, whiteout) + } + } +} diff --git a/pkg/v1/partial/README.md b/pkg/v1/partial/README.md new file mode 100644 index 0000000..53ebbc6 --- /dev/null +++ b/pkg/v1/partial/README.md @@ -0,0 +1,82 @@ +# `partial` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial) + +## Partial Implementations + +There are roughly two kinds of image representations: compressed and uncompressed. + +The implementations for these kinds of images are almost identical, with the only +major difference being how blobs (config and layers) are fetched. This common +code lives in this package, where you provide a _partial_ implementation of a +compressed or uncompressed image, and you get back a full `v1.Image` implementation. + +### Examples + +In a registry, blobs are compressed, so it's easiest to implement a `v1.Image` in terms +of compressed layers. `remote.remoteImage` does this by implementing `CompressedImageCore`: + +```go +type CompressedImageCore interface { + RawConfigFile() ([]byte, error) + MediaType() (types.MediaType, error) + RawManifest() ([]byte, error) + LayerByDigest(v1.Hash) (CompressedLayer, error) +} +``` + +In a tarball, blobs are (often) uncompressed, so it's easiest to implement a `v1.Image` in terms +of uncompressed layers. `tarball.uncompressedImage` does this by implementing `UncompressedImageCore`: + +```go +type UncompressedImageCore interface { + RawConfigFile() ([]byte, error) + MediaType() (types.MediaType, error) + LayerByDiffID(v1.Hash) (UncompressedLayer, error) +} +``` + +## Optional Methods + +Where possible, we access some information via optional methods as an optimization. + +### [`partial.Descriptor`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Descriptor) + +There are some properties of a [`Descriptor`](https://github.com/opencontainers/image-spec/blob/master/descriptor.md#properties) that aren't derivable from just image data: + +* `MediaType` +* `Platform` +* `URLs` +* `Annotations` + +For example, in a `tarball.Image`, there is a `LayerSources` field that contains +an entire layer descriptor with `URLs` information for foreign layers. This +information can be passed through to callers by implementing this optional +`Descriptor` method. + +See [`#654`](https://github.com/google/go-containerregistry/pull/654). + +### [`partial.UncompressedSize`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#UncompressedSize) + +Usually, you don't need to know the uncompressed size of a layer, since that +information isn't stored in a config file (just he sha256 is needed); however, +there are cases where it is very helpful to know the layer size, e.g. when +writing the uncompressed layer into a tarball. + +See [`#655`](https://github.com/google/go-containerregistry/pull/655). + +### [`partial.Exists`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/partial#Exists) + +We generally don't care about the existence of something as granular as a +layer, and would rather ensure all the invariants of an image are upheld via +the `validate` package. However, there are situations where we want to do a +quick smoke test to ensure that the underlying storage engine hasn't been +corrupted by something e.g. deleting files or blobs. Thus, we've exposed an +optional `Exists` method that does an existence check without actually reading +any bytes. + +The `remote` package implements this via `HEAD` requests. + +The `layout` package implements this via `os.Stat`. + +See [`#838`](https://github.com/google/go-containerregistry/pull/838). diff --git a/pkg/v1/partial/compressed.go b/pkg/v1/partial/compressed.go new file mode 100644 index 0000000..44989ac --- /dev/null +++ b/pkg/v1/partial/compressed.go @@ -0,0 +1,188 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "io" + + "github.com/google/go-containerregistry/internal/and" + "github.com/google/go-containerregistry/internal/compression" + "github.com/google/go-containerregistry/internal/gzip" + "github.com/google/go-containerregistry/internal/zstd" + comp "github.com/google/go-containerregistry/pkg/compression" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// CompressedLayer represents the bare minimum interface a natively +// compressed layer must implement for us to produce a v1.Layer +type CompressedLayer interface { + // Digest returns the Hash of the compressed layer. + Digest() (v1.Hash, error) + + // Compressed returns an io.ReadCloser for the compressed layer contents. + Compressed() (io.ReadCloser, error) + + // Size returns the compressed size of the Layer. + Size() (int64, error) + + // Returns the mediaType for the compressed Layer + MediaType() (types.MediaType, error) +} + +// compressedLayerExtender implements v1.Image using the compressed base properties. +type compressedLayerExtender struct { + CompressedLayer +} + +// Uncompressed implements v1.Layer +func (cle *compressedLayerExtender) Uncompressed() (io.ReadCloser, error) { + rc, err := cle.Compressed() + if err != nil { + return nil, err + } + + // Often, the "compressed" bytes are not actually-compressed. + // Peek at the first two bytes to determine whether it's correct to + // wrap this with gzip.UnzipReadCloser or zstd.UnzipReadCloser. + cp, pr, err := compression.PeekCompression(rc) + if err != nil { + return nil, err + } + + prc := &and.ReadCloser{ + Reader: pr, + CloseFunc: rc.Close, + } + + switch cp { + case comp.GZip: + return gzip.UnzipReadCloser(prc) + case comp.ZStd: + return zstd.UnzipReadCloser(prc) + default: + return prc, nil + } +} + +// DiffID implements v1.Layer +func (cle *compressedLayerExtender) DiffID() (v1.Hash, error) { + // If our nested CompressedLayer implements DiffID, + // then delegate to it instead. + if wdi, ok := cle.CompressedLayer.(WithDiffID); ok { + return wdi.DiffID() + } + r, err := cle.Uncompressed() + if err != nil { + return v1.Hash{}, err + } + defer r.Close() + h, _, err := v1.SHA256(r) + return h, err +} + +// CompressedToLayer fills in the missing methods from a CompressedLayer so that it implements v1.Layer +func CompressedToLayer(ul CompressedLayer) (v1.Layer, error) { + return &compressedLayerExtender{ul}, nil +} + +// CompressedImageCore represents the base minimum interface a natively +// compressed image must implement for us to produce a v1.Image. +type CompressedImageCore interface { + ImageCore + + // RawManifest returns the serialized bytes of the manifest. + RawManifest() ([]byte, error) + + // LayerByDigest is a variation on the v1.Image method, which returns + // a CompressedLayer instead. + LayerByDigest(v1.Hash) (CompressedLayer, error) +} + +// compressedImageExtender implements v1.Image by extending CompressedImageCore with the +// appropriate methods computed from the minimal core. +type compressedImageExtender struct { + CompressedImageCore +} + +// Assert that our extender type completes the v1.Image interface +var _ v1.Image = (*compressedImageExtender)(nil) + +// Digest implements v1.Image +func (i *compressedImageExtender) Digest() (v1.Hash, error) { + return Digest(i) +} + +// ConfigName implements v1.Image +func (i *compressedImageExtender) ConfigName() (v1.Hash, error) { + return ConfigName(i) +} + +// Layers implements v1.Image +func (i *compressedImageExtender) Layers() ([]v1.Layer, error) { + hs, err := FSLayers(i) + if err != nil { + return nil, err + } + ls := make([]v1.Layer, 0, len(hs)) + for _, h := range hs { + l, err := i.LayerByDigest(h) + if err != nil { + return nil, err + } + ls = append(ls, l) + } + return ls, nil +} + +// LayerByDigest implements v1.Image +func (i *compressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) { + cl, err := i.CompressedImageCore.LayerByDigest(h) + if err != nil { + return nil, err + } + return CompressedToLayer(cl) +} + +// LayerByDiffID implements v1.Image +func (i *compressedImageExtender) LayerByDiffID(h v1.Hash) (v1.Layer, error) { + h, err := DiffIDToBlob(i, h) + if err != nil { + return nil, err + } + return i.LayerByDigest(h) +} + +// ConfigFile implements v1.Image +func (i *compressedImageExtender) ConfigFile() (*v1.ConfigFile, error) { + return ConfigFile(i) +} + +// Manifest implements v1.Image +func (i *compressedImageExtender) Manifest() (*v1.Manifest, error) { + return Manifest(i) +} + +// Size implements v1.Image +func (i *compressedImageExtender) Size() (int64, error) { + return Size(i) +} + +// CompressedToImage fills in the missing methods from a CompressedImageCore so that it implements v1.Image +func CompressedToImage(cic CompressedImageCore) (v1.Image, error) { + return &compressedImageExtender{ + CompressedImageCore: cic, + }, nil +} diff --git a/pkg/v1/partial/compressed_test.go b/pkg/v1/partial/compressed_test.go new file mode 100644 index 0000000..bf6bff4 --- /dev/null +++ b/pkg/v1/partial/compressed_test.go @@ -0,0 +1,193 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial_test + +import ( + "io" + "net/http/httptest" + "net/url" + "path" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +// Remote leverages a lot of compressed partials. +func TestRemote(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + rnd, err := random.Image(1024, 3) + if err != nil { + t.Fatal(err) + } + + src := path.Join(u.Host, "test/compressed") + ref, err := name.ParseReference(src) + if err != nil { + t.Fatal(err) + } + if err := remote.Write(ref, rnd); err != nil { + t.Fatal(err) + } + + img, err := remote.Image(ref) + if err != nil { + t.Fatal(err) + } + if err := validate.Image(img); err != nil { + t.Fatal(err) + } + + cf, err := img.ConfigFile() + if err != nil { + t.Fatal(err) + } + m, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + layer, err := img.LayerByDiffID(cf.RootFS.DiffIDs[0]) + if err != nil { + t.Fatal(err) + } + d, err := layer.Digest() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(d, m.Layers[0].Digest); diff != "" { + t.Errorf("mismatched digest: %v", diff) + } + + ok, err := partial.Exists(layer) + if err != nil { + t.Fatal(err) + } + if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } + + cl, err := partial.ConfigLayer(img) + if err != nil { + t.Fatal(err) + } + if _, ok := cl.(*remote.MountableLayer); !ok { + t.Errorf("ConfigLayer() expected to be MountableLayer, got %T", cl) + } +} + +type noDiffID struct { + l v1.Layer +} + +func (l *noDiffID) Digest() (v1.Hash, error) { + return l.l.Digest() +} +func (l *noDiffID) Compressed() (io.ReadCloser, error) { + return l.l.Compressed() +} +func (l *noDiffID) Size() (int64, error) { + return l.l.Size() +} +func (l *noDiffID) MediaType() (types.MediaType, error) { + return l.l.MediaType() +} +func (l *noDiffID) Descriptor() (*v1.Descriptor, error) { + return partial.Descriptor(l.l) +} +func (l *noDiffID) UncompressedSize() (int64, error) { + return partial.UncompressedSize(l.l) +} + +func TestCompressedLayerExtender(t *testing.T) { + rnd, err := random.Layer(1000, types.OCILayer) + if err != nil { + t.Fatal(err) + } + l, err := partial.CompressedToLayer(&noDiffID{rnd}) + if err != nil { + t.Fatal(err) + } + + if err := compare.Layers(rnd, l); err != nil { + t.Fatalf("compare.Layers: %v", err) + } + if _, err := partial.Descriptor(l); err != nil { + t.Fatalf("partial.Descriptor: %v", err) + } + if _, err := partial.UncompressedSize(l); err != nil { + t.Fatalf("partial.UncompressedSize: %v", err) + } +} + +type compressedImage struct { + img v1.Image +} + +func (i *compressedImage) RawConfigFile() ([]byte, error) { + return i.img.RawConfigFile() +} + +func (i *compressedImage) MediaType() (types.MediaType, error) { + return i.img.MediaType() +} + +func (i *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) { + return i.img.LayerByDigest(h) +} + +func (i *compressedImage) RawManifest() ([]byte, error) { + return i.img.RawManifest() +} + +func (i *compressedImage) Descriptor() (*v1.Descriptor, error) { + return partial.Descriptor(i.img) +} + +func TestCompressed(t *testing.T) { + rnd, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + core := &compressedImage{rnd} + + img, err := partial.CompressedToImage(core) + if err != nil { + t.Fatal(err) + } + + if err := validate.Image(img); err != nil { + t.Fatalf("validate.Image: %v", err) + } + if _, err := partial.Descriptor(img); err != nil { + t.Fatalf("partial.Descriptor: %v", err) + } +} diff --git a/pkg/v1/partial/configlayer_test.go b/pkg/v1/partial/configlayer_test.go new file mode 100644 index 0000000..decdcab --- /dev/null +++ b/pkg/v1/partial/configlayer_test.go @@ -0,0 +1,139 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "fmt" + "io" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type testUIC struct { + UncompressedImageCore + configFile []byte +} + +func (t testUIC) RawConfigFile() ([]byte, error) { + return t.configFile, nil +} + +type testCIC struct { + CompressedImageCore + configFile []byte +} + +func (t testCIC) LayerByDigest(h v1.Hash) (CompressedLayer, error) { + return nil, fmt.Errorf("no layer by diff ID %v", h) +} + +func (t testCIC) RawConfigFile() ([]byte, error) { + return t.configFile, nil +} + +func TestConfigLayer(t *testing.T) { + cases := []v1.Image{ + &compressedImageExtender{ + CompressedImageCore: testCIC{ + configFile: []byte("{}"), + }, + }, + &uncompressedImageExtender{ + UncompressedImageCore: testUIC{ + configFile: []byte("{}"), + }, + }, + } + + for _, image := range cases { + hash, err := image.ConfigName() + if err != nil { + t.Fatalf("Error getting config name: %v", err) + } + + if _, err := image.LayerByDigest(hash); err == nil { + t.Error("LayerByDigest(config hash) returned nil error, wanted error") + } + + layer, err := ConfigLayer(image) + if err != nil { + t.Fatalf("ConfigLayer: %v", err) + } + lr, err := layer.Uncompressed() + if err != nil { + t.Fatalf("Error getting uncompressed layer: %v", err) + } + zr, err := layer.Compressed() + if err != nil { + t.Fatalf("Error getting compressed layer: %v", err) + } + + cfgLayerBytes, err := io.ReadAll(lr) + if err != nil { + t.Fatalf("Error reading config layer bytes: %v", err) + } + zcfgLayerBytes, err := io.ReadAll(zr) + if err != nil { + t.Fatalf("Error reading config layer bytes: %v", err) + } + + cfgFile, err := image.RawConfigFile() + if err != nil { + t.Fatalf("Error getting raw config file: %v", err) + } + + if string(cfgFile) != string(cfgLayerBytes) { + t.Errorf("Config file layer doesn't match raw config file") + } + if string(cfgFile) != string(zcfgLayerBytes) { + t.Errorf("Config file layer doesn't match raw config file") + } + + size, err := layer.Size() + if err != nil { + t.Fatalf("Error getting config layer size: %v", err) + } + if size != int64(len(cfgFile)) { + t.Errorf("Size() = %d, want %d", size, len(cfgFile)) + } + + digest, err := layer.Digest() + if err != nil { + t.Fatalf("Digest() = %v", err) + } + if digest != hash { + t.Errorf("ConfigLayer().Digest() != ConfigName(); %v, %v", digest, hash) + } + + diffid, err := layer.DiffID() + if err != nil { + t.Fatalf("DiffId() = %v", err) + } + if diffid != hash { + t.Errorf("ConfigLayer().DiffID() != ConfigName(); %v, %v", diffid, hash) + } + + mt, err := layer.MediaType() + if err != nil { + t.Fatalf("Error getting config layer media type: %v", err) + } + + if mt != types.OCIConfigJSON { + t.Errorf("MediaType() = %v, want %v", mt, types.OCIConfigJSON) + } + } +} diff --git a/pkg/v1/partial/doc.go b/pkg/v1/partial/doc.go new file mode 100644 index 0000000..153dfe4 --- /dev/null +++ b/pkg/v1/partial/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package partial defines methods for building up a v1.Image from +// minimal subsets that are sufficient for defining a v1.Image. +package partial diff --git a/pkg/v1/partial/image.go b/pkg/v1/partial/image.go new file mode 100644 index 0000000..c65f45e --- /dev/null +++ b/pkg/v1/partial/image.go @@ -0,0 +1,28 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// ImageCore is the core set of properties without which we cannot build a v1.Image +type ImageCore interface { + // RawConfigFile returns the serialized bytes of this image's config file. + RawConfigFile() ([]byte, error) + + // MediaType of this image's manifest. + MediaType() (types.MediaType, error) +} diff --git a/pkg/v1/partial/index.go b/pkg/v1/partial/index.go new file mode 100644 index 0000000..f17f274 --- /dev/null +++ b/pkg/v1/partial/index.go @@ -0,0 +1,85 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/match" +) + +// FindManifests given a v1.ImageIndex, find the manifests that fit the matcher. +func FindManifests(index v1.ImageIndex, matcher match.Matcher) ([]v1.Descriptor, error) { + // get the actual manifest list + indexManifest, err := index.IndexManifest() + if err != nil { + return nil, fmt.Errorf("unable to get raw index: %w", err) + } + manifests := []v1.Descriptor{} + // try to get the root of our image + for _, manifest := range indexManifest.Manifests { + if matcher(manifest) { + manifests = append(manifests, manifest) + } + } + return manifests, nil +} + +// FindImages given a v1.ImageIndex, find the images that fit the matcher. If a Descriptor +// matches the provider Matcher, but the referenced item is not an Image, ignores it. +// Only returns those that match the Matcher and are images. +func FindImages(index v1.ImageIndex, matcher match.Matcher) ([]v1.Image, error) { + matches := []v1.Image{} + manifests, err := FindManifests(index, matcher) + if err != nil { + return nil, err + } + for _, desc := range manifests { + // if it is not an image, ignore it + if !desc.MediaType.IsImage() { + continue + } + img, err := index.Image(desc.Digest) + if err != nil { + return nil, err + } + matches = append(matches, img) + } + return matches, nil +} + +// FindIndexes given a v1.ImageIndex, find the indexes that fit the matcher. If a Descriptor +// matches the provider Matcher, but the referenced item is not an Index, ignores it. +// Only returns those that match the Matcher and are indexes. +func FindIndexes(index v1.ImageIndex, matcher match.Matcher) ([]v1.ImageIndex, error) { + matches := []v1.ImageIndex{} + manifests, err := FindManifests(index, matcher) + if err != nil { + return nil, err + } + for _, desc := range manifests { + if !desc.MediaType.IsIndex() { + continue + } + // if it is not an index, ignore it + idx, err := index.ImageIndex(desc.Digest) + if err != nil { + return nil, err + } + matches = append(matches, idx) + } + return matches, nil +} diff --git a/pkg/v1/partial/index_test.go b/pkg/v1/partial/index_test.go new file mode 100644 index 0000000..c289758 --- /dev/null +++ b/pkg/v1/partial/index_test.go @@ -0,0 +1,119 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestFindManifests(t *testing.T) { + ii, err := random.Index(100, 5, 6) // random image of 6 manifests, each having 5 layers of size 100 + if err != nil { + t.Fatal("could not create random index:", err) + } + m, _ := ii.IndexManifest() + digest := m.Manifests[0].Digest + + matcher := func(desc v1.Descriptor) bool { + return desc.Digest != digest + } + + descriptors, err := partial.FindManifests(ii, matcher) + expected := len(m.Manifests) - 1 + switch { + case err != nil: + t.Error("unexpected error:", err) + case len(descriptors) != expected: + t.Errorf("failed on manifests, actual %d, expected %d", len(descriptors), expected) + } +} + +func TestFindImages(t *testing.T) { + // create our imageindex with which to work + ii, err := random.Index(100, 5, 6) // random image of 6 manifests, each having 5 layers of size 100 + if err != nil { + t.Fatal("could not create random index:", err) + } + m, _ := ii.IndexManifest() + digest := m.Manifests[0].Digest + + matcher := func(desc v1.Descriptor) bool { + return desc.Digest != digest + } + images, err := partial.FindImages(ii, matcher) + expected := len(m.Manifests) - 1 + switch { + case err != nil: + t.Error("unexpected error:", err) + case len(images) != expected: + t.Errorf("failed on images, actual %d, expected %d", len(images), expected) + } +} + +func TestFindIndexes(t *testing.T) { + // there is no utility to generate an index of indexes, so we need to create one + // base index + var ( + indexCount = 5 + imageCount = 7 + ) + base := empty.Index + // we now have 5 indexes and 5 images, so wrap them into a single index + adds := []mutate.IndexAddendum{} + for i := 0; i < indexCount; i++ { + ii, err := random.Index(100, 1, 1) + if err != nil { + t.Fatalf("%d: unable to create random index: %v", i, err) + } + adds = append(adds, mutate.IndexAddendum{ + Add: ii, + Descriptor: v1.Descriptor{ + MediaType: types.OCIImageIndex, + }, + }) + } + for i := 0; i < imageCount; i++ { + img, err := random.Image(100, 1) + if err != nil { + t.Fatalf("%d: unable to create random image: %v", i, err) + } + adds = append(adds, mutate.IndexAddendum{ + Add: img, + Descriptor: v1.Descriptor{ + MediaType: types.OCIManifestSchema1, + }, + }) + } + + // just see if it finds all of the indexes + matcher := func(desc v1.Descriptor) bool { + return true + } + index := mutate.AppendManifests(base, adds...) + idxes, err := partial.FindIndexes(index, matcher) + switch { + case err != nil: + t.Error("unexpected error:", err) + case len(idxes) != indexCount: + t.Errorf("failed on index, actual %d, expected %d", len(idxes), indexCount) + } +} diff --git a/pkg/v1/partial/uncompressed.go b/pkg/v1/partial/uncompressed.go new file mode 100644 index 0000000..df20d3a --- /dev/null +++ b/pkg/v1/partial/uncompressed.go @@ -0,0 +1,223 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "bytes" + "io" + "sync" + + "github.com/google/go-containerregistry/internal/gzip" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// UncompressedLayer represents the bare minimum interface a natively +// uncompressed layer must implement for us to produce a v1.Layer +type UncompressedLayer interface { + // DiffID returns the Hash of the uncompressed layer. + DiffID() (v1.Hash, error) + + // Uncompressed returns an io.ReadCloser for the uncompressed layer contents. + Uncompressed() (io.ReadCloser, error) + + // Returns the mediaType for the compressed Layer + MediaType() (types.MediaType, error) +} + +// uncompressedLayerExtender implements v1.Image using the uncompressed base properties. +type uncompressedLayerExtender struct { + UncompressedLayer + // Memoize size/hash so that the methods aren't twice as + // expensive as doing this manually. + hash v1.Hash + size int64 + hashSizeError error + once sync.Once +} + +// Compressed implements v1.Layer +func (ule *uncompressedLayerExtender) Compressed() (io.ReadCloser, error) { + u, err := ule.Uncompressed() + if err != nil { + return nil, err + } + return gzip.ReadCloser(u), nil +} + +// Digest implements v1.Layer +func (ule *uncompressedLayerExtender) Digest() (v1.Hash, error) { + ule.calcSizeHash() + return ule.hash, ule.hashSizeError +} + +// Size implements v1.Layer +func (ule *uncompressedLayerExtender) Size() (int64, error) { + ule.calcSizeHash() + return ule.size, ule.hashSizeError +} + +func (ule *uncompressedLayerExtender) calcSizeHash() { + ule.once.Do(func() { + var r io.ReadCloser + r, ule.hashSizeError = ule.Compressed() + if ule.hashSizeError != nil { + return + } + defer r.Close() + ule.hash, ule.size, ule.hashSizeError = v1.SHA256(r) + }) +} + +// UncompressedToLayer fills in the missing methods from an UncompressedLayer so that it implements v1.Layer +func UncompressedToLayer(ul UncompressedLayer) (v1.Layer, error) { + return &uncompressedLayerExtender{UncompressedLayer: ul}, nil +} + +// UncompressedImageCore represents the bare minimum interface a natively +// uncompressed image must implement for us to produce a v1.Image +type UncompressedImageCore interface { + ImageCore + + // LayerByDiffID is a variation on the v1.Image method, which returns + // an UncompressedLayer instead. + LayerByDiffID(v1.Hash) (UncompressedLayer, error) +} + +// UncompressedToImage fills in the missing methods from an UncompressedImageCore so that it implements v1.Image. +func UncompressedToImage(uic UncompressedImageCore) (v1.Image, error) { + return &uncompressedImageExtender{ + UncompressedImageCore: uic, + }, nil +} + +// uncompressedImageExtender implements v1.Image by extending UncompressedImageCore with the +// appropriate methods computed from the minimal core. +type uncompressedImageExtender struct { + UncompressedImageCore + + lock sync.Mutex + manifest *v1.Manifest +} + +// Assert that our extender type completes the v1.Image interface +var _ v1.Image = (*uncompressedImageExtender)(nil) + +// Digest implements v1.Image +func (i *uncompressedImageExtender) Digest() (v1.Hash, error) { + return Digest(i) +} + +// Manifest implements v1.Image +func (i *uncompressedImageExtender) Manifest() (*v1.Manifest, error) { + i.lock.Lock() + defer i.lock.Unlock() + if i.manifest != nil { + return i.manifest, nil + } + + b, err := i.RawConfigFile() + if err != nil { + return nil, err + } + + cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b)) + if err != nil { + return nil, err + } + + m := &v1.Manifest{ + SchemaVersion: 2, + MediaType: types.DockerManifestSchema2, + Config: v1.Descriptor{ + MediaType: types.DockerConfigJSON, + Size: cfgSize, + Digest: cfgHash, + }, + } + + ls, err := i.Layers() + if err != nil { + return nil, err + } + + m.Layers = make([]v1.Descriptor, len(ls)) + for i, l := range ls { + desc, err := Descriptor(l) + if err != nil { + return nil, err + } + + m.Layers[i] = *desc + } + + i.manifest = m + return i.manifest, nil +} + +// RawManifest implements v1.Image +func (i *uncompressedImageExtender) RawManifest() ([]byte, error) { + return RawManifest(i) +} + +// Size implements v1.Image +func (i *uncompressedImageExtender) Size() (int64, error) { + return Size(i) +} + +// ConfigName implements v1.Image +func (i *uncompressedImageExtender) ConfigName() (v1.Hash, error) { + return ConfigName(i) +} + +// ConfigFile implements v1.Image +func (i *uncompressedImageExtender) ConfigFile() (*v1.ConfigFile, error) { + return ConfigFile(i) +} + +// Layers implements v1.Image +func (i *uncompressedImageExtender) Layers() ([]v1.Layer, error) { + diffIDs, err := DiffIDs(i) + if err != nil { + return nil, err + } + ls := make([]v1.Layer, 0, len(diffIDs)) + for _, h := range diffIDs { + l, err := i.LayerByDiffID(h) + if err != nil { + return nil, err + } + ls = append(ls, l) + } + return ls, nil +} + +// LayerByDiffID implements v1.Image +func (i *uncompressedImageExtender) LayerByDiffID(diffID v1.Hash) (v1.Layer, error) { + ul, err := i.UncompressedImageCore.LayerByDiffID(diffID) + if err != nil { + return nil, err + } + return UncompressedToLayer(ul) +} + +// LayerByDigest implements v1.Image +func (i *uncompressedImageExtender) LayerByDigest(h v1.Hash) (v1.Layer, error) { + diffID, err := BlobToDiffID(i, h) + if err != nil { + return nil, err + } + return i.LayerByDiffID(diffID) +} diff --git a/pkg/v1/partial/uncompressed_test.go b/pkg/v1/partial/uncompressed_test.go new file mode 100644 index 0000000..ce5a6fa --- /dev/null +++ b/pkg/v1/partial/uncompressed_test.go @@ -0,0 +1,233 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial_test + +import ( + "io" + "os" + "testing" + + "github.com/google/go-containerregistry/internal/compare" + legacy "github.com/google/go-containerregistry/pkg/legacy/tarball" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +// foreignLayer implements both partial.Describable and partial.UncompressedLayer. +type foreignLayer struct { + wrapped v1.Layer +} + +func (l *foreignLayer) Digest() (v1.Hash, error) { + return l.wrapped.Digest() +} + +func (l *foreignLayer) Size() (int64, error) { + return l.wrapped.Size() +} + +func (l *foreignLayer) MediaType() (types.MediaType, error) { + return types.DockerForeignLayer, nil +} + +func (l *foreignLayer) Uncompressed() (io.ReadCloser, error) { + return l.wrapped.Uncompressed() +} + +func (l *foreignLayer) DiffID() (v1.Hash, error) { + return l.wrapped.DiffID() +} + +func (l *foreignLayer) Descriptor() (*v1.Descriptor, error) { + r, err := l.wrapped.Compressed() + if err != nil { + return nil, err + } + h, sz, err := v1.SHA256(r) + if err != nil { + return nil, err + } + return &v1.Descriptor{ + Digest: h, + Size: sz, + MediaType: types.DockerForeignLayer, + URLs: []string{"http://example.com"}, + }, nil +} + +func (l *foreignLayer) UncompressedSize() (int64, error) { + return partial.UncompressedSize(l.wrapped) +} + +func TestUncompressedLayer(t *testing.T) { + randLayer, err := random.Layer(1024, types.DockerForeignLayer) + if err != nil { + t.Fatal(err) + } + l := &foreignLayer{randLayer} + + desc, err := partial.Descriptor(l) + if err != nil { + t.Fatal(err) + } + + if want, got := desc.URLs[0], "http://example.com"; want != got { + t.Errorf("URLs[0] = %s != %s", got, want) + } + + layer, err := partial.UncompressedToLayer(l) + if err != nil { + t.Fatal(err) + } + + if err := validate.Layer(layer); err != nil { + t.Errorf("validate.Layer: %v", err) + } + if _, err := partial.UncompressedSize(layer); err != nil { + t.Errorf("partial.UncompressedSize: %v", err) + } +} + +// legacy/tarball.Write + tarball.Image leverages a lot of uncompressed partials. +// +// This is cribbed from pkg/legacy/tarball just to get intra-package coverage. +func TestLegacyWrite(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + layer with Descriptor(). + randImage, err := random.Image(256, 2) + if err != nil { + t.Fatalf("Error creating random image: %v", err) + } + randLayer, err := random.Layer(1024, types.DockerForeignLayer) + if err != nil { + t.Fatal(err) + } + l, err := partial.UncompressedToLayer(&foreignLayer{randLayer}) + if err != nil { + t.Fatal(err) + } + img, err := mutate.AppendLayers(randImage, l) + if err != nil { + t.Fatal(err) + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag: %v", err) + } + o, err := os.Create(fp.Name()) + if err != nil { + t.Fatalf("Error creating %q to write image tarball: %v", fp.Name(), err) + } + defer o.Close() + if err := legacy.Write(tag, img, o); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + + // Make sure the image is valid and can be loaded. + // Load it both by nil and by its name. + for _, it := range []*name.Tag{nil, &tag} { + tarImage, err := tarball.ImageFromPath(fp.Name(), it) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + if err := compare.Images(img, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } + + // Try loading a different tag, it should error. + fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error generating tag: %v", err) + } + if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil { + t.Errorf("Expected error loading tag %v from image", fakeTag) + } +} + +type uncompressedImage struct { + img v1.Image +} + +func (i *uncompressedImage) RawConfigFile() ([]byte, error) { + return i.img.RawConfigFile() +} + +func (i *uncompressedImage) MediaType() (types.MediaType, error) { + return i.img.MediaType() +} + +func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) { + return i.img.LayerByDiffID(h) +} + +func (i *uncompressedImage) Descriptor() (*v1.Descriptor, error) { + return partial.Descriptor(i.img) +} + +func TestUncompressed(t *testing.T) { + rnd, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + core := &uncompressedImage{rnd} + + img, err := partial.UncompressedToImage(core) + if err != nil { + t.Fatal(err) + } + + if err := validate.Image(img); err != nil { + t.Fatalf("validate.Image: %v", err) + } + if _, err := partial.Descriptor(img); err != nil { + t.Fatalf("partial.Descriptor: %v", err) + } + + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + layer, err := partial.UncompressedToLayer(&fastpathLayer{layers[0]}) + if err != nil { + t.Fatal(err) + } + + ok, err := partial.Exists(layer) + if err != nil { + t.Fatal(err) + } + if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } +} diff --git a/pkg/v1/partial/with.go b/pkg/v1/partial/with.go new file mode 100644 index 0000000..c8b22b3 --- /dev/null +++ b/pkg/v1/partial/with.go @@ -0,0 +1,436 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// WithRawConfigFile defines the subset of v1.Image used by these helper methods +type WithRawConfigFile interface { + // RawConfigFile returns the serialized bytes of this image's config file. + RawConfigFile() ([]byte, error) +} + +// ConfigFile is a helper for implementing v1.Image +func ConfigFile(i WithRawConfigFile) (*v1.ConfigFile, error) { + b, err := i.RawConfigFile() + if err != nil { + return nil, err + } + return v1.ParseConfigFile(bytes.NewReader(b)) +} + +// ConfigName is a helper for implementing v1.Image +func ConfigName(i WithRawConfigFile) (v1.Hash, error) { + b, err := i.RawConfigFile() + if err != nil { + return v1.Hash{}, err + } + h, _, err := v1.SHA256(bytes.NewReader(b)) + return h, err +} + +type configLayer struct { + hash v1.Hash + content []byte +} + +// Digest implements v1.Layer +func (cl *configLayer) Digest() (v1.Hash, error) { + return cl.hash, nil +} + +// DiffID implements v1.Layer +func (cl *configLayer) DiffID() (v1.Hash, error) { + return cl.hash, nil +} + +// Uncompressed implements v1.Layer +func (cl *configLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(cl.content)), nil +} + +// Compressed implements v1.Layer +func (cl *configLayer) Compressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(cl.content)), nil +} + +// Size implements v1.Layer +func (cl *configLayer) Size() (int64, error) { + return int64(len(cl.content)), nil +} + +func (cl *configLayer) MediaType() (types.MediaType, error) { + // Defaulting this to OCIConfigJSON as it should remain + // backwards compatible with DockerConfigJSON + return types.OCIConfigJSON, nil +} + +var _ v1.Layer = (*configLayer)(nil) + +// withConfigLayer allows partial image implementations to provide a layer +// for their config file. +type withConfigLayer interface { + ConfigLayer() (v1.Layer, error) +} + +// ConfigLayer implements v1.Layer from the raw config bytes. +// This is so that clients (e.g. remote) can access the config as a blob. +// +// Images that want to return a specific layer implementation can implement +// withConfigLayer. +func ConfigLayer(i WithRawConfigFile) (v1.Layer, error) { + if wcl, ok := unwrap(i).(withConfigLayer); ok { + return wcl.ConfigLayer() + } + + h, err := ConfigName(i) + if err != nil { + return nil, err + } + rcfg, err := i.RawConfigFile() + if err != nil { + return nil, err + } + return &configLayer{ + hash: h, + content: rcfg, + }, nil +} + +// WithConfigFile defines the subset of v1.Image used by these helper methods +type WithConfigFile interface { + // ConfigFile returns this image's config file. + ConfigFile() (*v1.ConfigFile, error) +} + +// DiffIDs is a helper for implementing v1.Image +func DiffIDs(i WithConfigFile) ([]v1.Hash, error) { + cfg, err := i.ConfigFile() + if err != nil { + return nil, err + } + return cfg.RootFS.DiffIDs, nil +} + +// RawConfigFile is a helper for implementing v1.Image +func RawConfigFile(i WithConfigFile) ([]byte, error) { + cfg, err := i.ConfigFile() + if err != nil { + return nil, err + } + return json.Marshal(cfg) +} + +// WithRawManifest defines the subset of v1.Image used by these helper methods +type WithRawManifest interface { + // RawManifest returns the serialized bytes of this image's config file. + RawManifest() ([]byte, error) +} + +// Digest is a helper for implementing v1.Image +func Digest(i WithRawManifest) (v1.Hash, error) { + mb, err := i.RawManifest() + if err != nil { + return v1.Hash{}, err + } + digest, _, err := v1.SHA256(bytes.NewReader(mb)) + return digest, err +} + +// Manifest is a helper for implementing v1.Image +func Manifest(i WithRawManifest) (*v1.Manifest, error) { + b, err := i.RawManifest() + if err != nil { + return nil, err + } + return v1.ParseManifest(bytes.NewReader(b)) +} + +// WithManifest defines the subset of v1.Image used by these helper methods +type WithManifest interface { + // Manifest returns this image's Manifest object. + Manifest() (*v1.Manifest, error) +} + +// RawManifest is a helper for implementing v1.Image +func RawManifest(i WithManifest) ([]byte, error) { + m, err := i.Manifest() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +// Size is a helper for implementing v1.Image +func Size(i WithRawManifest) (int64, error) { + b, err := i.RawManifest() + if err != nil { + return -1, err + } + return int64(len(b)), nil +} + +// FSLayers is a helper for implementing v1.Image +func FSLayers(i WithManifest) ([]v1.Hash, error) { + m, err := i.Manifest() + if err != nil { + return nil, err + } + fsl := make([]v1.Hash, len(m.Layers)) + for i, l := range m.Layers { + fsl[i] = l.Digest + } + return fsl, nil +} + +// BlobSize is a helper for implementing v1.Image +func BlobSize(i WithManifest, h v1.Hash) (int64, error) { + d, err := BlobDescriptor(i, h) + if err != nil { + return -1, err + } + return d.Size, nil +} + +// BlobDescriptor is a helper for implementing v1.Image +func BlobDescriptor(i WithManifest, h v1.Hash) (*v1.Descriptor, error) { + m, err := i.Manifest() + if err != nil { + return nil, err + } + + if m.Config.Digest == h { + return &m.Config, nil + } + + for _, l := range m.Layers { + if l.Digest == h { + return &l, nil + } + } + return nil, fmt.Errorf("blob %v not found", h) +} + +// WithManifestAndConfigFile defines the subset of v1.Image used by these helper methods +type WithManifestAndConfigFile interface { + WithConfigFile + + // Manifest returns this image's Manifest object. + Manifest() (*v1.Manifest, error) +} + +// BlobToDiffID is a helper for mapping between compressed +// and uncompressed blob hashes. +func BlobToDiffID(i WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) { + blobs, err := FSLayers(i) + if err != nil { + return v1.Hash{}, err + } + diffIDs, err := DiffIDs(i) + if err != nil { + return v1.Hash{}, err + } + if len(blobs) != len(diffIDs) { + return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs)) + } + for i, blob := range blobs { + if blob == h { + return diffIDs[i], nil + } + } + return v1.Hash{}, fmt.Errorf("unknown blob %v", h) +} + +// DiffIDToBlob is a helper for mapping between uncompressed +// and compressed blob hashes. +func DiffIDToBlob(wm WithManifestAndConfigFile, h v1.Hash) (v1.Hash, error) { + blobs, err := FSLayers(wm) + if err != nil { + return v1.Hash{}, err + } + diffIDs, err := DiffIDs(wm) + if err != nil { + return v1.Hash{}, err + } + if len(blobs) != len(diffIDs) { + return v1.Hash{}, fmt.Errorf("mismatched fs layers (%d) and diff ids (%d)", len(blobs), len(diffIDs)) + } + for i, diffID := range diffIDs { + if diffID == h { + return blobs[i], nil + } + } + return v1.Hash{}, fmt.Errorf("unknown diffID %v", h) +} + +// WithDiffID defines the subset of v1.Layer for exposing the DiffID method. +type WithDiffID interface { + DiffID() (v1.Hash, error) +} + +// withDescriptor allows partial layer implementations to provide a layer +// descriptor to the partial image manifest builder. This allows partial +// uncompressed layers to provide foreign layer metadata like URLs to the +// uncompressed image manifest. +type withDescriptor interface { + Descriptor() (*v1.Descriptor, error) +} + +// Describable represents something for which we can produce a v1.Descriptor. +type Describable interface { + Digest() (v1.Hash, error) + MediaType() (types.MediaType, error) + Size() (int64, error) +} + +// Descriptor returns a v1.Descriptor given a Describable. It also encodes +// some logic for unwrapping things that have been wrapped by +// CompressedToLayer, UncompressedToLayer, CompressedToImage, or +// UncompressedToImage. +func Descriptor(d Describable) (*v1.Descriptor, error) { + // If Describable implements Descriptor itself, return that. + if wd, ok := unwrap(d).(withDescriptor); ok { + return wd.Descriptor() + } + + // If all else fails, compute the descriptor from the individual methods. + var ( + desc v1.Descriptor + err error + ) + + if desc.Size, err = d.Size(); err != nil { + return nil, err + } + if desc.Digest, err = d.Digest(); err != nil { + return nil, err + } + if desc.MediaType, err = d.MediaType(); err != nil { + return nil, err + } + if wat, ok := d.(withArtifactType); ok { + if desc.ArtifactType, err = wat.ArtifactType(); err != nil { + return nil, err + } + } else { + if wrm, ok := d.(WithRawManifest); ok && desc.MediaType.IsImage() { + mf, _ := Manifest(wrm) + // Failing to parse as a manifest should just be ignored. + // The manifest might not be valid, and that's okay. + if mf != nil && !mf.Config.MediaType.IsConfig() { + desc.ArtifactType = string(mf.Config.MediaType) + } + } + } + + return &desc, nil +} + +type withArtifactType interface { + ArtifactType() (string, error) +} + +type withUncompressedSize interface { + UncompressedSize() (int64, error) +} + +// UncompressedSize returns the size of the Uncompressed layer. If the +// underlying implementation doesn't implement UncompressedSize directly, +// this will compute the uncompressedSize by reading everything returned +// by Compressed(). This is potentially expensive and may consume the contents +// for streaming layers. +func UncompressedSize(l v1.Layer) (int64, error) { + // If the layer implements UncompressedSize itself, return that. + if wus, ok := unwrap(l).(withUncompressedSize); ok { + return wus.UncompressedSize() + } + + // The layer doesn't implement UncompressedSize, we need to compute it. + rc, err := l.Uncompressed() + if err != nil { + return -1, err + } + defer rc.Close() + + return io.Copy(io.Discard, rc) +} + +type withExists interface { + Exists() (bool, error) +} + +// Exists checks to see if a layer exists. This is a hack to work around the +// mistakes of the partial package. Don't use this. +func Exists(l v1.Layer) (bool, error) { + // If the layer implements Exists itself, return that. + if we, ok := unwrap(l).(withExists); ok { + return we.Exists() + } + + // The layer doesn't implement Exists, so we hope that calling Compressed() + // is enough to trigger an error if the layer does not exist. + rc, err := l.Compressed() + if err != nil { + return false, err + } + defer rc.Close() + + // We may want to try actually reading a single byte, but if we need to do + // that, we should just fix this hack. + return true, nil +} + +// Recursively unwrap our wrappers so that we can check for the original implementation. +// We might want to expose this? +func unwrap(i any) any { + if ule, ok := i.(*uncompressedLayerExtender); ok { + return unwrap(ule.UncompressedLayer) + } + if cle, ok := i.(*compressedLayerExtender); ok { + return unwrap(cle.CompressedLayer) + } + if uie, ok := i.(*uncompressedImageExtender); ok { + return unwrap(uie.UncompressedImageCore) + } + if cie, ok := i.(*compressedImageExtender); ok { + return unwrap(cie.CompressedImageCore) + } + return i +} + +// ArtifactType returns the artifact type for the given manifest. +// +// If the manifest reports its own artifact type, that's returned, otherwise +// the manifest is parsed and, if successful, its config.mediaType is returned. +func ArtifactType(w WithManifest) (string, error) { + if wat, ok := w.(withArtifactType); ok { + return wat.ArtifactType() + } + mf, _ := w.Manifest() + // Failing to parse as a manifest should just be ignored. + // The manifest might not be valid, and that's okay. + if mf != nil && !mf.Config.MediaType.IsConfig() { + return string(mf.Config.MediaType), nil + } + return "", nil +} diff --git a/pkg/v1/partial/with_test.go b/pkg/v1/partial/with_test.go new file mode 100644 index 0000000..7796bd9 --- /dev/null +++ b/pkg/v1/partial/with_test.go @@ -0,0 +1,246 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package partial_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestRawConfigFile(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + part, err := partial.RawConfigFile(img) + if err != nil { + t.Fatal(err) + } + + method, err := img.RawConfigFile() + if err != nil { + t.Fatal(err) + } + + if string(part) != string(method) { + t.Errorf("mismatched config file: %s vs %s", part, method) + } +} + +func TestDigest(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + part, err := partial.Digest(img) + if err != nil { + t.Fatal(err) + } + + method, err := img.Digest() + if err != nil { + t.Fatal(err) + } + + if part != method { + t.Errorf("mismatched digest: %s vs %s", part, method) + } +} + +func TestManifest(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + part, err := partial.Manifest(img) + if err != nil { + t.Fatal(err) + } + + method, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(part, method); diff != "" { + t.Errorf("mismatched manifest: %v", diff) + } +} + +func TestSize(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + + part, err := partial.Size(img) + if err != nil { + t.Fatal(err) + } + + method, err := img.Size() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(part, method); diff != "" { + t.Errorf("mismatched size: %v", diff) + } +} + +func TestDiffIDToBlob(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + cf, err := img.ConfigFile() + if err != nil { + t.Fatal(err) + } + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + want, err := layers[0].Digest() + if err != nil { + t.Fatal(err) + } + got, err := partial.DiffIDToBlob(img, cf.RootFS.DiffIDs[0]) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("mismatched digest: %v", diff) + } + + if _, err := partial.DiffIDToBlob(img, want); err == nil { + t.Errorf("expected err, got nil") + } +} + +func TestBlobToDiffID(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + cf, err := img.ConfigFile() + if err != nil { + t.Fatal(err) + } + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + d, err := layers[0].Digest() + if err != nil { + t.Fatal(err) + } + want := cf.RootFS.DiffIDs[0] + got, err := partial.BlobToDiffID(img, d) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("mismatched digest: %v", diff) + } + + if _, err := partial.BlobToDiffID(img, want); err == nil { + t.Errorf("expected err, got nil") + } +} + +func TestBlobSize(t *testing.T) { + img, err := random.Image(1024, 1) + if err != nil { + t.Fatal(err) + } + m, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + want := m.Layers[0].Size + got, err := partial.BlobSize(img, m.Layers[0].Digest) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("mismatched blob size: %v", diff) + } + + if _, err := partial.BlobSize(img, v1.Hash{}); err == nil { + t.Errorf("expected err, got nil") + } +} + +type fastpathLayer struct { + v1.Layer +} + +func (l *fastpathLayer) UncompressedSize() (int64, error) { + return 100, nil +} + +func (l *fastpathLayer) Exists() (bool, error) { + return true, nil +} + +func TestUncompressedSize(t *testing.T) { + randLayer, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + fpl := &fastpathLayer{randLayer} + us, err := partial.UncompressedSize(fpl) + if err != nil { + t.Fatal(err) + } + if got, want := us, int64(100); got != want { + t.Errorf("UncompressedSize() = %d != %d", got, want) + } +} + +func TestExists(t *testing.T) { + randLayer, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + fpl := &fastpathLayer{randLayer} + ok, err := partial.Exists(fpl) + if err != nil { + t.Fatal(err) + } + if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } + + ok, err = partial.Exists(randLayer) + if err != nil { + t.Fatal(err) + } + if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } +} diff --git a/pkg/v1/platform.go b/pkg/v1/platform.go new file mode 100644 index 0000000..59ca402 --- /dev/null +++ b/pkg/v1/platform.go @@ -0,0 +1,149 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +import ( + "fmt" + "sort" + "strings" +) + +// Platform represents the target os/arch for an image. +type Platform struct { + Architecture string `json:"architecture"` + OS string `json:"os"` + OSVersion string `json:"os.version,omitempty"` + OSFeatures []string `json:"os.features,omitempty"` + Variant string `json:"variant,omitempty"` + Features []string `json:"features,omitempty"` +} + +func (p Platform) String() string { + if p.OS == "" { + return "" + } + var b strings.Builder + b.WriteString(p.OS) + if p.Architecture != "" { + b.WriteString("/") + b.WriteString(p.Architecture) + } + if p.Variant != "" { + b.WriteString("/") + b.WriteString(p.Variant) + } + if p.OSVersion != "" { + b.WriteString(":") + b.WriteString(p.OSVersion) + } + return b.String() +} + +// ParsePlatform parses a string representing a Platform, if possible. +func ParsePlatform(s string) (*Platform, error) { + var p Platform + parts := strings.Split(strings.TrimSpace(s), ":") + if len(parts) == 2 { + p.OSVersion = parts[1] + } + parts = strings.Split(parts[0], "/") + if len(parts) > 0 { + p.OS = parts[0] + } + if len(parts) > 1 { + p.Architecture = parts[1] + } + if len(parts) > 2 { + p.Variant = parts[2] + } + if len(parts) > 3 { + return nil, fmt.Errorf("too many slashes in platform spec: %s", s) + } + return &p, nil +} + +// Equals returns true if the given platform is semantically equivalent to this one. +// The order of Features and OSFeatures is not important. +func (p Platform) Equals(o Platform) bool { + return p.OS == o.OS && + p.Architecture == o.Architecture && + p.Variant == o.Variant && + p.OSVersion == o.OSVersion && + stringSliceEqualIgnoreOrder(p.OSFeatures, o.OSFeatures) && + stringSliceEqualIgnoreOrder(p.Features, o.Features) +} + +// Satisfies returns true if this Platform "satisfies" the given spec Platform. +// +// Note that this is different from Equals and that Satisfies is not reflexive. +// +// The given spec represents "requirements" such that any missing values in the +// spec are not compared. +// +// For OSFeatures and Features, Satisfies will return true if this Platform's +// fields contain a superset of the values in the spec's fields (order ignored). +func (p Platform) Satisfies(spec Platform) bool { + return satisfies(spec.OS, p.OS) && + satisfies(spec.Architecture, p.Architecture) && + satisfies(spec.Variant, p.Variant) && + satisfies(spec.OSVersion, p.OSVersion) && + satisfiesList(spec.OSFeatures, p.OSFeatures) && + satisfiesList(spec.Features, p.Features) +} + +func satisfies(want, have string) bool { + return want == "" || want == have +} + +func satisfiesList(want, have []string) bool { + if len(want) == 0 { + return true + } + + set := map[string]struct{}{} + for _, h := range have { + set[h] = struct{}{} + } + + for _, w := range want { + if _, ok := set[w]; !ok { + return false + } + } + + return true +} + +// stringSliceEqual compares 2 string slices and returns if their contents are identical. +func stringSliceEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, elm := range a { + if elm != b[i] { + return false + } + } + return true +} + +// stringSliceEqualIgnoreOrder compares 2 string slices and returns if their contents are identical, ignoring order +func stringSliceEqualIgnoreOrder(a, b []string) bool { + if a != nil && b != nil { + sort.Strings(a) + sort.Strings(b) + } + return stringSliceEqual(a, b) +} diff --git a/pkg/v1/platform_test.go b/pkg/v1/platform_test.go new file mode 100644 index 0000000..80c67ed --- /dev/null +++ b/pkg/v1/platform_test.go @@ -0,0 +1,235 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +func TestPlatformString(t *testing.T) { + for _, c := range []struct { + plat v1.Platform + want string + }{{ + v1.Platform{}, + "", + }, { + v1.Platform{OS: "linux"}, + "linux", + }, { + v1.Platform{OS: "linux", Architecture: "amd64"}, + "linux/amd64", + }, { + v1.Platform{OS: "linux", Architecture: "amd64", Variant: "v7"}, + "linux/amd64/v7", + }, { + v1.Platform{OS: "linux", Architecture: "amd64", OSVersion: "1.2.3.4"}, + "linux/amd64:1.2.3.4", + }, { + v1.Platform{OS: "linux", Architecture: "amd64", OSVersion: "1.2.3.4", OSFeatures: []string{"a", "b"}, Features: []string{"c", "d"}}, + "linux/amd64:1.2.3.4", + }} { + if got := c.plat.String(); got != c.want { + t.Errorf("got %q, want %q", got, c.want) + } + + if len(c.plat.OSFeatures) > 0 || len(c.plat.Features) > 0 { + // If these values are set, roundtripping back to the + // Platform will be lossy, and we expect that. + continue + } + + back, err := v1.ParsePlatform(c.plat.String()) + if err != nil { + t.Errorf("ParsePlatform(%q): %v", c.plat, err) + } + if d := cmp.Diff(&c.plat, back); d != "" { + t.Errorf("ParsePlatform(%q) diff:\n%s", c.plat.String(), d) + } + } + + // Known bad examples. + for _, s := range []string{ + "linux/amd64/v7/s9", // too many slashes + } { + got, err := v1.ParsePlatform(s) + if err == nil { + t.Errorf("ParsePlatform(%q) wanted error; got %v", s, got) + } + } +} + +func TestPlatformEquals(t *testing.T) { + tests := []struct { + a, b v1.Platform + equal bool + }{{ + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "arm64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "darwin"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "3.6"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "ubuntu"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"ac", "bd"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"b", "a"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"ac", "bd"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"b", "a"}}, + true, + }} + for i, tt := range tests { + if equal := tt.a.Equals(tt.b); equal != tt.equal { + t.Errorf("%d: mismatched was %v expected %v; original (-want +got) %s", i, equal, tt.equal, cmp.Diff(tt.a, tt.b)) + } + } +} + +func TestPlatformSatisfies(t *testing.T) { + tests := []struct { + have, spec v1.Platform + sat bool + }{{ + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "arm64", OS: "linux"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "darwin"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "5.0"}, + v1.Platform{Architecture: "amd64", OS: "linux", OSVersion: "3.6"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "ubuntu"}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + v1.Platform{Architecture: "amd64", OS: "linux", Variant: "pios"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"ac", "bd"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", OSFeatures: []string{"b", "a"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux"}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux"}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + true, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"ac", "bd"}}, + false, + }, { + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"a", "b"}}, + v1.Platform{Architecture: "amd64", OS: "linux", Features: []string{"b", "a"}}, + true, + }} + for i, tt := range tests { + if sat := tt.have.Satisfies(tt.spec); sat != tt.sat { + t.Errorf("%d: mismatched was %v expected %v; original (-want +got) %s", i, sat, tt.sat, cmp.Diff(tt.have, tt.spec)) + } + } +} diff --git a/pkg/v1/progress.go b/pkg/v1/progress.go new file mode 100644 index 0000000..844f04d --- /dev/null +++ b/pkg/v1/progress.go @@ -0,0 +1,25 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v1 + +// Update representation of an update of transfer progress. Some functions +// in this module can take a channel to which updates will be sent while a +// transfer is in progress. +// +k8s:deepcopy-gen=false +type Update struct { + Total int64 + Complete int64 + Error error +} diff --git a/pkg/v1/random/doc.go b/pkg/v1/random/doc.go new file mode 100644 index 0000000..d371276 --- /dev/null +++ b/pkg/v1/random/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package random provides a facility for synthesizing pseudo-random images. +package random diff --git a/pkg/v1/random/image.go b/pkg/v1/random/image.go new file mode 100644 index 0000000..4b28913 --- /dev/null +++ b/pkg/v1/random/image.go @@ -0,0 +1,116 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "archive/tar" + "bytes" + "crypto" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + mrand "math/rand" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// uncompressedLayer implements partial.UncompressedLayer from raw bytes. +type uncompressedLayer struct { + diffID v1.Hash + mediaType types.MediaType + content []byte +} + +// DiffID implements partial.UncompressedLayer +func (ul *uncompressedLayer) DiffID() (v1.Hash, error) { + return ul.diffID, nil +} + +// Uncompressed implements partial.UncompressedLayer +func (ul *uncompressedLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBuffer(ul.content)), nil +} + +// MediaType returns the media type of the layer +func (ul *uncompressedLayer) MediaType() (types.MediaType, error) { + return ul.mediaType, nil +} + +var _ partial.UncompressedLayer = (*uncompressedLayer)(nil) + +// Image returns a pseudo-randomly generated Image. +func Image(byteSize, layers int64) (v1.Image, error) { + adds := make([]mutate.Addendum, 0, 5) + for i := int64(0); i < layers; i++ { + layer, err := Layer(byteSize, types.DockerLayer) + if err != nil { + return nil, err + } + adds = append(adds, mutate.Addendum{ + Layer: layer, + History: v1.History{ + Author: "random.Image", + Comment: fmt.Sprintf("this is a random history %d of %d", i, layers), + CreatedBy: "random", + Created: v1.Time{Time: time.Now()}, + }, + }) + } + + return mutate.Append(empty.Image, adds...) +} + +// Layer returns a layer with pseudo-randomly generated content. +func Layer(byteSize int64, mt types.MediaType) (v1.Layer, error) { + fileName := fmt.Sprintf("random_file_%d.txt", mrand.Int()) //nolint: gosec + + // Hash the contents as we write it out to the buffer. + var b bytes.Buffer + hasher := crypto.SHA256.New() + mw := io.MultiWriter(&b, hasher) + + // Write a single file with a random name and random contents. + tw := tar.NewWriter(mw) + if err := tw.WriteHeader(&tar.Header{ + Name: fileName, + Size: byteSize, + Typeflag: tar.TypeReg, + }); err != nil { + return nil, err + } + if _, err := io.CopyN(tw, rand.Reader, byteSize); err != nil { + return nil, err + } + if err := tw.Close(); err != nil { + return nil, err + } + + h := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(hasher.Sum(make([]byte, 0, hasher.Size()))), + } + + return partial.UncompressedToLayer(&uncompressedLayer{ + diffID: h, + mediaType: mt, + content: b.Bytes(), + }) +} diff --git a/pkg/v1/random/image_test.go b/pkg/v1/random/image_test.go new file mode 100644 index 0000000..8f30bc7 --- /dev/null +++ b/pkg/v1/random/image_test.go @@ -0,0 +1,129 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "archive/tar" + "errors" + "io" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestManifestAndConfig(t *testing.T) { + want := int64(12) + img, err := Image(1024, want) + if err != nil { + t.Fatalf("Error loading image: %v", err) + } + manifest, err := img.Manifest() + if err != nil { + t.Fatalf("Error loading manifest: %v", err) + } + if got := int64(len(manifest.Layers)); got != want { + t.Fatalf("num layers; got %v, want %v", got, want) + } + + config, err := img.ConfigFile() + if err != nil { + t.Fatalf("Error loading config file: %v", err) + } + if got := int64(len(config.RootFS.DiffIDs)); got != want { + t.Fatalf("num diff ids; got %v, want %v", got, want) + } + + if err := validate.Image(img); err != nil { + t.Errorf("failed to validate: %v", err) + } +} + +func TestTarLayer(t *testing.T) { + img, err := Image(1024, 5) + if err != nil { + t.Fatalf("Image: %v", err) + } + layers, err := img.Layers() + if err != nil { + t.Fatalf("Layers: %v", err) + } + if len(layers) != 5 { + t.Errorf("Got %d layers, want 5", len(layers)) + } + for i, l := range layers { + mediaType, err := l.MediaType() + if err != nil { + t.Fatalf("MediaType: %v", err) + } + if got, want := mediaType, types.DockerLayer; got != want { + t.Fatalf("MediaType(); got %q, want %q", got, want) + } + + rc, err := l.Uncompressed() + if err != nil { + t.Errorf("Uncompressed(%d): %v", i, err) + } + defer rc.Close() + tr := tar.NewReader(rc) + if _, err := tr.Next(); err != nil { + t.Errorf("tar.Next: %v", err) + } + + if n, err := io.Copy(io.Discard, tr); err != nil { + t.Errorf("Reading tar layer: %v", err) + } else if n != 1024 { + t.Errorf("Layer %d was %d bytes, want 1024", i, n) + } + + if _, err := tr.Next(); !errors.Is(err, io.EOF) { + t.Errorf("Layer contained more files; got %v, want EOF", err) + } + } +} + +func TestRandomLayer(t *testing.T) { + l, err := Layer(1024, types.DockerLayer) + if err != nil { + t.Fatalf("Layer: %v", err) + } + mediaType, err := l.MediaType() + if err != nil { + t.Fatalf("MediaType: %v", err) + } + if got, want := mediaType, types.DockerLayer; got != want { + t.Errorf("MediaType(); got %q, want %q", got, want) + } + + rc, err := l.Uncompressed() + if err != nil { + t.Fatalf("Uncompressed(): %v", err) + } + defer rc.Close() + tr := tar.NewReader(rc) + if _, err := tr.Next(); err != nil { + t.Fatalf("tar.Next: %v", err) + } + + if n, err := io.Copy(io.Discard, tr); err != nil { + t.Errorf("Reading tar layer: %v", err) + } else if n != 1024 { + t.Errorf("Layer was %d bytes, want 1024", n) + } + + if _, err := tr.Next(); !errors.Is(err, io.EOF) { + t.Errorf("Layer contained more files; got %v, want EOF", err) + } +} diff --git a/pkg/v1/random/index.go b/pkg/v1/random/index.go new file mode 100644 index 0000000..89a8843 --- /dev/null +++ b/pkg/v1/random/index.go @@ -0,0 +1,111 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "bytes" + "encoding/json" + "fmt" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type randomIndex struct { + images map[v1.Hash]v1.Image + manifest *v1.IndexManifest +} + +// Index returns a pseudo-randomly generated ImageIndex with count images, each +// having the given number of layers of size byteSize. +func Index(byteSize, layers, count int64) (v1.ImageIndex, error) { + manifest := v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Manifests: []v1.Descriptor{}, + } + + images := make(map[v1.Hash]v1.Image) + for i := int64(0); i < count; i++ { + img, err := Image(byteSize, layers) + if err != nil { + return nil, err + } + + rawManifest, err := img.RawManifest() + if err != nil { + return nil, err + } + digest, size, err := v1.SHA256(bytes.NewReader(rawManifest)) + if err != nil { + return nil, err + } + mediaType, err := img.MediaType() + if err != nil { + return nil, err + } + + manifest.Manifests = append(manifest.Manifests, v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }) + + images[digest] = img + } + + return &randomIndex{ + images: images, + manifest: &manifest, + }, nil +} + +func (i *randomIndex) MediaType() (types.MediaType, error) { + return i.manifest.MediaType, nil +} + +func (i *randomIndex) Digest() (v1.Hash, error) { + return partial.Digest(i) +} + +func (i *randomIndex) Size() (int64, error) { + return partial.Size(i) +} + +func (i *randomIndex) IndexManifest() (*v1.IndexManifest, error) { + return i.manifest, nil +} + +func (i *randomIndex) RawManifest() ([]byte, error) { + m, err := i.IndexManifest() + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +func (i *randomIndex) Image(h v1.Hash) (v1.Image, error) { + if img, ok := i.images[h]; ok { + return img, nil + } + + return nil, fmt.Errorf("image not found: %v", h) +} + +func (i *randomIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + // This is a single level index (for now?). + return nil, fmt.Errorf("image not found: %v", h) +} diff --git a/pkg/v1/random/index_test.go b/pkg/v1/random/index_test.go new file mode 100644 index 0000000..73e744b --- /dev/null +++ b/pkg/v1/random/index_test.go @@ -0,0 +1,64 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package random + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestRandomIndex(t *testing.T) { + ii, err := Index(1024, 5, 3) + if err != nil { + t.Fatalf("Error loading index: %v", err) + } + + if err := validate.Index(ii); err != nil { + t.Errorf("validate.Index() = %v", err) + } + + digest, err := ii.Digest() + if err != nil { + t.Fatalf("Digest(): unexpected err: %v", err) + } + + if _, err := ii.Image(digest); err == nil { + t.Errorf("Image(%s): expected err, got nil", digest) + } + + if _, err := ii.ImageIndex(digest); err == nil { + t.Errorf("ImageIndex(%s): expected err, got nil", digest) + } + + mt, err := ii.MediaType() + if err != nil { + t.Errorf("MediaType(): unexpected err: %v", err) + } + + if got, want := mt, types.OCIImageIndex; got != want { + t.Errorf("MediaType(): got: %v, want: %v", got, want) + } + + man, err := ii.IndexManifest() + if err != nil { + t.Errorf("IndexManifest(): unexpected err: %v", err) + } + + if got, want := man.MediaType, types.OCIImageIndex; got != want { + t.Errorf("MediaType: got: %v, want: %v", got, want) + } +} diff --git a/pkg/v1/remote/README.md b/pkg/v1/remote/README.md new file mode 100644 index 0000000..c1e81b3 --- /dev/null +++ b/pkg/v1/remote/README.md @@ -0,0 +1,117 @@ +# `remote` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote) + +The `remote` package implements a client for accessing a registry, +per the [OCI distribution spec](https://github.com/opencontainers/distribution-spec/blob/master/spec.md). + +It leans heavily on the lower level [`transport`](/pkg/v1/remote/transport) package, which handles the +authentication handshake and structured errors. + +## Usage + +```go +package main + +import ( + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func main() { + ref, err := name.ParseReference("gcr.io/google-containers/pause") + if err != nil { + panic(err) + } + + img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + panic(err) + } + + // do stuff with img +} +``` + +## Structure + +<p align="center"> + <img src="/images/remote.dot.svg" /> +</p> + + +## Background + +There are a lot of confusingly similar terms that come up when talking about images in registries. + +### Anatomy of an image + +In general... + +* A tag refers to an image manifest. +* An image manifest references a config file and an orderered list of _compressed_ layers by sha256 digest. +* A config file references an ordered list of _uncompressed_ layers by sha256 digest and contains runtime configuration. +* The sha256 digest of the config file is the [image id](https://github.com/opencontainers/image-spec/blob/master/config.md#imageid) for the image. + +For example, an image with two layers would look something like this: + +![image anatomy](/images/image-anatomy.dot.svg) + +### Anatomy of an index + +In the normal case, an [index](https://github.com/opencontainers/image-spec/blob/master/image-index.md) is used to represent a multi-platform image. +This was the original use case for a [manifest +list](https://docs.docker.com/registry/spec/manifest-v2-2/#manifest-list). + +![image index anatomy](/images/index-anatomy.dot.svg) + +It is possible for an index to reference another index, per the OCI +[image-spec](https://github.com/opencontainers/image-spec/blob/master/media-types.md#compatibility-matrix). +In theory, both an image and image index can reference arbitrary things via +[descriptors](https://github.com/opencontainers/image-spec/blob/master/descriptor.md), +e.g. see the [image layout +example](https://github.com/opencontainers/image-spec/blob/master/image-layout.md#index-example), +which references an application/xml file from an image index. + +That could look something like this: + +![strange image index anatomy](/images/index-anatomy-strange.dot.svg) + +Using a recursive index like this might not be possible with all registries, +but this flexibility allows for some interesting applications, e.g. the +[OCI Artifacts](https://github.com/opencontainers/artifacts) effort. + +### Anatomy of an image upload + +The structure of an image requires a delicate ordering when uploading an image to a registry. +Below is a (slightly simplified) figure that describes how an image is prepared for upload +to a registry and how the data flows between various artifacts: + +![upload](/images/upload.dot.svg) + +Note that: + +* A config file references the uncompressed layer contents by sha256. +* A manifest references the compressed layer contents by sha256 and the size of the layer. +* A manifest references the config file contents by sha256 and the size of the file. + +It follows that during an upload, we need to upload layers before the config file, +and we need to upload the config file before the manifest. + +Sometimes, we know all of this information ahead of time, (e.g. when copying from remote.Image), +so the ordering is less important. + +In other cases, e.g. when using a [`stream.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream#Layer), +we can't compute anything until we have already uploaded the layer, so we need to be careful about ordering. + +## Caveats + +### schema 1 + +This package does not support schema 1 images, see [`#377`](https://github.com/google/go-containerregistry/issues/377), +however, it's possible to do _something_ useful with them via [`remote.Get`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote#Get), +which doesn't try to interpret what is returned by the registry. + +[`crane.Copy`](https://godoc.org/github.com/google/go-containerregistry/pkg/crane#Copy) takes advantage of this to implement support for copying schema 1 images, +see [here](https://github.com/google/go-containerregistry/blob/main/pkg/internal/legacy/copy.go). diff --git a/pkg/v1/remote/catalog.go b/pkg/v1/remote/catalog.go new file mode 100644 index 0000000..eb4306f --- /dev/null +++ b/pkg/v1/remote/catalog.go @@ -0,0 +1,154 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +type catalog struct { + Repos []string `json:"repositories"` +} + +// CatalogPage calls /_catalog, returning the list of repositories on the registry. +func CatalogPage(target name.Registry, last string, n int, options ...Option) ([]string, error) { + o, err := makeOptions(target, options...) + if err != nil { + return nil, err + } + + scopes := []string{target.Scope(transport.PullScope)} + tr, err := transport.NewWithContext(o.context, target, o.auth, o.transport, scopes) + if err != nil { + return nil, err + } + + query := fmt.Sprintf("last=%s&n=%d", url.QueryEscape(last), n) + + uri := url.URL{ + Scheme: target.Scheme(), + Host: target.RegistryStr(), + Path: "/v2/_catalog", + RawQuery: query, + } + + client := http.Client{Transport: tr} + req, err := http.NewRequest(http.MethodGet, uri.String(), nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req.WithContext(o.context)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + var parsed catalog + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return nil, err + } + + return parsed.Repos, nil +} + +// Catalog calls /_catalog, returning the list of repositories on the registry. +func Catalog(ctx context.Context, target name.Registry, options ...Option) ([]string, error) { + o, err := makeOptions(target, options...) + if err != nil { + return nil, err + } + + scopes := []string{target.Scope(transport.PullScope)} + tr, err := transport.NewWithContext(o.context, target, o.auth, o.transport, scopes) + if err != nil { + return nil, err + } + + uri := &url.URL{ + Scheme: target.Scheme(), + Host: target.RegistryStr(), + Path: "/v2/_catalog", + } + + if o.pageSize > 0 { + uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize) + } + + client := http.Client{Transport: tr} + + // WithContext overrides the ctx passed directly. + if o.context != context.Background() { + ctx = o.context + } + + var ( + parsed catalog + repoList []string + ) + + // get responses until there is no next page + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + req, err := http.NewRequest("GET", uri.String(), nil) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return nil, err + } + if err := resp.Body.Close(); err != nil { + return nil, err + } + + repoList = append(repoList, parsed.Repos...) + + uri, err = getNextPageURL(resp) + if err != nil { + return nil, err + } + // no next page + if uri == nil { + break + } + } + return repoList, nil +} diff --git a/pkg/v1/remote/catalog_test.go b/pkg/v1/remote/catalog_test.go new file mode 100644 index 0000000..0a90bf6 --- /dev/null +++ b/pkg/v1/remote/catalog_test.go @@ -0,0 +1,183 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" +) + +func TestCatalogPage(t *testing.T) { + cases := []struct { + name string + responseBody []byte + wantErr bool + wantRepos []string + }{{ + name: "success", + responseBody: []byte(`{"repositories":["test/test","foo/bar"]}`), + wantErr: false, + wantRepos: []string{"test/test", "foo/bar"}, + }, { + name: "not json", + responseBody: []byte("notjson"), + wantErr: true, + }} + // TODO: add test cases for pagination + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + catalogPath := "/v2/_catalog" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case catalogPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Write(tc.responseBody) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + reg, err := name.NewRegistry(u.Host) + if err != nil { + t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err) + } + + repos, err := CatalogPage(reg, "", 100) + if (err != nil) != tc.wantErr { + t.Errorf("CatalogPage() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + + if diff := cmp.Diff(tc.wantRepos, repos); diff != "" { + t.Errorf("CatalogPage() wrong repos (-want +got) = %s", diff) + } + }) + } +} + +func TestCatalog(t *testing.T) { + cases := []struct { + name string + pages [][]byte + wantErr bool + wantRepos []string + }{{ + name: "success", + pages: [][]byte{ + []byte(`{"repositories":["test/one","test/two"]}`), + []byte(`{"repositories":["test/three","test/four"]}`), + }, + wantErr: false, + wantRepos: []string{"test/one", "test/two", "test/three", "test/four"}, + }, { + name: "not json", + pages: [][]byte{[]byte("notjson")}, + wantErr: true, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + catalogPath := "/v2/_catalog" + pageTwo := "/v2/_catalog_two" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page := 0 + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case pageTwo: + page = 1 + fallthrough + case catalogPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + if page == 0 { + w.Header().Set("Link", fmt.Sprintf("<%s>", pageTwo)) + } + w.Write(tc.pages[page]) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + reg, err := name.NewRegistry(u.Host) + if err != nil { + t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err) + } + + repos, err := Catalog(context.Background(), reg) + if (err != nil) != tc.wantErr { + t.Errorf("Catalog() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + + if diff := cmp.Diff(tc.wantRepos, repos); diff != "" { + t.Errorf("Catalog() wrong repos (-want +got) = %s", diff) + } + }) + } +} + +func TestCancelledCatalog(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + reg, err := name.NewRegistry(u.Host) + if err != nil { + t.Fatalf("name.NewRegistry(%v) = %v", u.Host, err) + } + + _, err = Catalog(ctx, reg) + if want, got := context.Canceled, err; !errors.Is(got, want) { + t.Errorf("wanted %v got %v", want, got) + } +} diff --git a/pkg/v1/remote/check.go b/pkg/v1/remote/check.go new file mode 100644 index 0000000..b4395c2 --- /dev/null +++ b/pkg/v1/remote/check.go @@ -0,0 +1,72 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// CheckPushPermission returns an error if the given keychain cannot authorize +// a push operation to the given ref. +// +// This can be useful to check whether the caller has permission to push an +// image before doing work to construct the image. +// +// TODO(#412): Remove the need for this method. +func CheckPushPermission(ref name.Reference, kc authn.Keychain, t http.RoundTripper) error { + auth, err := kc.Resolve(ref.Context().Registry) + if err != nil { + return fmt.Errorf("resolving authorization for %v failed: %w", ref.Context().Registry, err) + } + + scopes := []string{ref.Scope(transport.PushScope)} + tr, err := transport.NewWithContext(context.TODO(), ref.Context().Registry, auth, t, scopes) + if err != nil { + return fmt.Errorf("creating push check transport for %v failed: %w", ref.Context().Registry, err) + } + // TODO(jasonhall): Against GCR, just doing the token handshake is + // enough, but this doesn't extend to Dockerhub + // (https://github.com/docker/hub-feedback/issues/1771), so we actually + // need to initiate an upload to tell whether the credentials can + // authorize a push. Figure out how to return early here when we can, + // to avoid a roundtrip for spec-compliant registries. + w := writer{ + repo: ref.Context(), + client: &http.Client{Transport: tr}, + } + loc, _, err := w.initiateUpload(context.Background(), "", "", "") + if loc != "" { + // Since we're only initiating the upload to check whether we + // can, we should attempt to cancel it, in case initiating + // reserves some resources on the server. We shouldn't wait for + // cancelling to complete, and we don't care if it fails. + go w.cancelUpload(loc) + } + return err +} + +func (w *writer) cancelUpload(loc string) { + req, err := http.NewRequest(http.MethodDelete, loc, nil) + if err != nil { + return + } + _, _ = w.client.Do(req) +} diff --git a/pkg/v1/remote/check_e2e_test.go b/pkg/v1/remote/check_e2e_test.go new file mode 100644 index 0000000..a302230 --- /dev/null +++ b/pkg/v1/remote/check_e2e_test.go @@ -0,0 +1,46 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build integration +// +build integration + +package remote + +import ( + "net/http" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" +) + +func TestCheckPushPermission_Real(t *testing.T) { + // Tests should not run in an environment where these registries can + // be pushed to. + for _, r := range []name.Reference{ + name.MustParseReference("ubuntu"), + name.MustParseReference("google/cloud-sdk"), + name.MustParseReference("microsoft/dotnet:sdk"), + name.MustParseReference("gcr.io/non-existent-project/made-up"), + name.MustParseReference("gcr.io/google-containers/foo"), + name.MustParseReference("quay.io/username/reponame"), + } { + t.Run(r.String(), func(t *testing.T) { + t.Parallel() + if err := CheckPushPermission(r, authn.DefaultKeychain, http.DefaultTransport); err == nil { + t.Errorf("CheckPushPermission(%s) returned nil", r) + } + }) + } +} diff --git a/pkg/v1/remote/check_test.go b/pkg/v1/remote/check_test.go new file mode 100644 index 0000000..2f76e12 --- /dev/null +++ b/pkg/v1/remote/check_test.go @@ -0,0 +1,76 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" +) + +func TestCheckPushPermission(t *testing.T) { + for _, c := range []struct { + status int + wantErr bool + }{{ + http.StatusCreated, + false, + }, { + http.StatusAccepted, + false, + }, { + http.StatusForbidden, + true, + }, { + http.StatusBadRequest, + true, + }} { + expectedRepo := "write/time" + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + somewhereElse := fmt.Sprintf("/v2/%s/blobs/uploads/somewhere/else", expectedRepo) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + w.Header().Set("Location", "somewhere/else") + http.Error(w, "", c.status) + case somewhereElse: + if r.Method != http.MethodDelete { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete) + } + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + ref := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + if err := CheckPushPermission(ref, authn.DefaultKeychain, http.DefaultTransport); (err != nil) != c.wantErr { + t.Errorf("CheckPermission(%d): got error = %v, want err = %t", c.status, err, c.wantErr) + } + } +} diff --git a/pkg/v1/remote/delete.go b/pkg/v1/remote/delete.go new file mode 100644 index 0000000..74a06fd --- /dev/null +++ b/pkg/v1/remote/delete.go @@ -0,0 +1,61 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// Delete removes the specified image reference from the remote registry. +func Delete(ref name.Reference, options ...Option) error { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return err + } + scopes := []string{ref.Scope(transport.DeleteScope)} + tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + c := &http.Client{Transport: tr} + + u := url.URL{ + Scheme: ref.Context().Registry.Scheme(), + Host: ref.Context().RegistryStr(), + Path: fmt.Sprintf("/v2/%s/manifests/%s", ref.Context().RepositoryStr(), ref.Identifier()), + } + + req, err := http.NewRequest(http.MethodDelete, u.String(), nil) + if err != nil { + return err + } + + resp, err := c.Do(req.WithContext(o.context)) + if err != nil { + return err + } + defer resp.Body.Close() + + return transport.CheckError(resp, http.StatusOK, http.StatusAccepted) + + // TODO(jason): If the manifest had a `subject`, and if the registry + // doesn't support Referrers, update the index pointed to by the + // subject's fallback tag to remove the descriptor for this manifest. +} diff --git a/pkg/v1/remote/delete_test.go b/pkg/v1/remote/delete_test.go new file mode 100644 index 0000000..4918e16 --- /dev/null +++ b/pkg/v1/remote/delete_test.go @@ -0,0 +1,89 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/name" +) + +func TestDelete(t *testing.T) { + expectedRepo := "write/time" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodDelete { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete) + } + http.Error(w, "Deleted", http.StatusOK) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := Delete(tag); err != nil { + t.Errorf("Delete() = %v", err) + } +} + +func TestDeleteBadStatus(t *testing.T) { + expectedRepo := "write/time" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodDelete { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodDelete) + } + http.Error(w, "Boom Goes Server", http.StatusInternalServerError) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := Delete(tag); err == nil { + t.Error("Delete() = nil; wanted error") + } +} diff --git a/pkg/v1/remote/descriptor.go b/pkg/v1/remote/descriptor.go new file mode 100644 index 0000000..78919d7 --- /dev/null +++ b/pkg/v1/remote/descriptor.go @@ -0,0 +1,511 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/internal/verify" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// ErrSchema1 indicates that we received a schema1 manifest from the registry. +// This library doesn't have plans to support this legacy image format: +// https://github.com/google/go-containerregistry/issues/377 +type ErrSchema1 struct { + schema string +} + +// newErrSchema1 returns an ErrSchema1 with the unexpected MediaType. +func newErrSchema1(schema types.MediaType) error { + return &ErrSchema1{ + schema: string(schema), + } +} + +// Error implements error. +func (e *ErrSchema1) Error() string { + return fmt.Sprintf("unsupported MediaType: %q, see https://github.com/google/go-containerregistry/issues/377", e.schema) +} + +// Descriptor provides access to metadata about remote artifact and accessors +// for efficiently converting it into a v1.Image or v1.ImageIndex. +type Descriptor struct { + fetcher + v1.Descriptor + Manifest []byte + + // So we can share this implementation with Image. + platform v1.Platform +} + +// RawManifest exists to satisfy the Taggable interface. +func (d *Descriptor) RawManifest() ([]byte, error) { + return d.Manifest, nil +} + +// Get returns a remote.Descriptor for the given reference. The response from +// the registry is left un-interpreted, for the most part. This is useful for +// querying what kind of artifact a reference represents. +// +// See Head if you don't need the response body. +func Get(ref name.Reference, options ...Option) (*Descriptor, error) { + acceptable := []types.MediaType{ + // Just to look at them. + types.DockerManifestSchema1, + types.DockerManifestSchema1Signed, + } + acceptable = append(acceptable, acceptableImageMediaTypes...) + acceptable = append(acceptable, acceptableIndexMediaTypes...) + return get(ref, acceptable, options...) +} + +// Head returns a v1.Descriptor for the given reference by issuing a HEAD +// request. +// +// Note that the server response will not have a body, so any errors encountered +// should be retried with Get to get more details. +func Head(ref name.Reference, options ...Option) (*v1.Descriptor, error) { + acceptable := []types.MediaType{ + // Just to look at them. + types.DockerManifestSchema1, + types.DockerManifestSchema1Signed, + } + acceptable = append(acceptable, acceptableImageMediaTypes...) + acceptable = append(acceptable, acceptableIndexMediaTypes...) + + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + + f, err := makeFetcher(ref, o) + if err != nil { + return nil, err + } + + return f.headManifest(ref, acceptable) +} + +// Handle options and fetch the manifest with the acceptable MediaTypes in the +// Accept header. +func get(ref name.Reference, acceptable []types.MediaType, options ...Option) (*Descriptor, error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + f, err := makeFetcher(ref, o) + if err != nil { + return nil, err + } + b, desc, err := f.fetchManifest(ref, acceptable) + if err != nil { + return nil, err + } + return &Descriptor{ + fetcher: *f, + Manifest: b, + Descriptor: *desc, + platform: o.platform, + }, nil +} + +// Image converts the Descriptor into a v1.Image. +// +// If the fetched artifact is already an image, it will just return it. +// +// If the fetched artifact is an index, it will attempt to resolve the index to +// a child image with the appropriate platform. +// +// See WithPlatform to set the desired platform. +func (d *Descriptor) Image() (v1.Image, error) { + switch d.MediaType { + case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: + // We don't care to support schema 1 images: + // https://github.com/google/go-containerregistry/issues/377 + return nil, newErrSchema1(d.MediaType) + case types.OCIImageIndex, types.DockerManifestList: + // We want an image but the registry has an index, resolve it to an image. + return d.remoteIndex().imageByPlatform(d.platform) + case types.OCIManifestSchema1, types.DockerManifestSchema2: + // These are expected. Enumerated here to allow a default case. + default: + // We could just return an error here, but some registries (e.g. static + // registries) don't set the Content-Type headers correctly, so instead... + logs.Warn.Printf("Unexpected media type for Image(): %s", d.MediaType) + } + + // Wrap the v1.Layers returned by this v1.Image in a hint for downstream + // remote.Write calls to facilitate cross-repo "mounting". + imgCore, err := partial.CompressedToImage(d.remoteImage()) + if err != nil { + return nil, err + } + return &mountableImage{ + Image: imgCore, + Reference: d.Ref, + }, nil +} + +// ImageIndex converts the Descriptor into a v1.ImageIndex. +func (d *Descriptor) ImageIndex() (v1.ImageIndex, error) { + switch d.MediaType { + case types.DockerManifestSchema1, types.DockerManifestSchema1Signed: + // We don't care to support schema 1 images: + // https://github.com/google/go-containerregistry/issues/377 + return nil, newErrSchema1(d.MediaType) + case types.OCIManifestSchema1, types.DockerManifestSchema2: + // We want an index but the registry has an image, nothing we can do. + return nil, fmt.Errorf("unexpected media type for ImageIndex(): %s; call Image() instead", d.MediaType) + case types.OCIImageIndex, types.DockerManifestList: + // These are expected. + default: + // We could just return an error here, but some registries (e.g. static + // registries) don't set the Content-Type headers correctly, so instead... + logs.Warn.Printf("Unexpected media type for ImageIndex(): %s", d.MediaType) + } + return d.remoteIndex(), nil +} + +func (d *Descriptor) remoteImage() *remoteImage { + return &remoteImage{ + fetcher: d.fetcher, + manifest: d.Manifest, + mediaType: d.MediaType, + descriptor: &d.Descriptor, + } +} + +func (d *Descriptor) remoteIndex() *remoteIndex { + return &remoteIndex{ + fetcher: d.fetcher, + manifest: d.Manifest, + mediaType: d.MediaType, + descriptor: &d.Descriptor, + } +} + +// fetcher implements methods for reading from a registry. +type fetcher struct { + Ref name.Reference + Client *http.Client + context context.Context +} + +func makeFetcher(ref name.Reference, o *options) (*fetcher, error) { + tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, []string{ref.Scope(transport.PullScope)}) + if err != nil { + return nil, err + } + return &fetcher{ + Ref: ref, + Client: &http.Client{Transport: tr}, + context: o.context, + }, nil +} + +// url returns a url.Url for the specified path in the context of this remote image reference. +func (f *fetcher) url(resource, identifier string) url.URL { + return url.URL{ + Scheme: f.Ref.Context().Registry.Scheme(), + Host: f.Ref.Context().RegistryStr(), + Path: fmt.Sprintf("/v2/%s/%s/%s", f.Ref.Context().RepositoryStr(), resource, identifier), + } +} + +// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema +func fallbackTag(d name.Digest) name.Tag { + return d.Context().Tag(strings.Replace(d.DigestStr(), ":", "-", 1)) +} + +func (f *fetcher) fetchReferrers(ctx context.Context, filter map[string]string, d name.Digest) (*v1.IndexManifest, error) { + // Check the Referrers API endpoint first. + u := f.url("referrers", d.DigestStr()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", string(types.OCIImageIndex)) + + resp, err := f.Client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil { + return nil, err + } + if resp.StatusCode == http.StatusOK { + var im v1.IndexManifest + if err := json.NewDecoder(resp.Body).Decode(&im); err != nil { + return nil, err + } + return filterReferrersResponse(filter, &im), nil + } + + // The registry doesn't support the Referrers API endpoint, so we'll use the fallback tag scheme. + b, _, err := f.fetchManifest(fallbackTag(d), []types.MediaType{types.OCIImageIndex}) + if err != nil { + return nil, err + } + var terr *transport.Error + if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound { + // Not found just means there are no attachments yet. Start with an empty manifest. + return &v1.IndexManifest{MediaType: types.OCIImageIndex}, nil + } + + var im v1.IndexManifest + if err := json.Unmarshal(b, &im); err != nil { + return nil, err + } + + return filterReferrersResponse(filter, &im), nil +} + +func (f *fetcher) fetchManifest(ref name.Reference, acceptable []types.MediaType) ([]byte, *v1.Descriptor, error) { + u := f.url("manifests", ref.Identifier()) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, nil, err + } + accept := []string{} + for _, mt := range acceptable { + accept = append(accept, string(mt)) + } + req.Header.Set("Accept", strings.Join(accept, ",")) + + resp, err := f.Client.Do(req.WithContext(f.context)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, nil, err + } + + manifest, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + + digest, size, err := v1.SHA256(bytes.NewReader(manifest)) + if err != nil { + return nil, nil, err + } + + mediaType := types.MediaType(resp.Header.Get("Content-Type")) + contentDigest, err := v1.NewHash(resp.Header.Get("Docker-Content-Digest")) + if err == nil && mediaType == types.DockerManifestSchema1Signed { + // If we can parse the digest from the header, and it's a signed schema 1 + // manifest, let's use that for the digest to appease older registries. + digest = contentDigest + } + + // Validate the digest matches what we asked for, if pulling by digest. + if dgst, ok := ref.(name.Digest); ok { + if digest.String() != dgst.DigestStr() { + return nil, nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref) + } + } + + var artifactType string + mf, _ := v1.ParseManifest(bytes.NewReader(manifest)) + // Failing to parse as a manifest should just be ignored. + // The manifest might not be valid, and that's okay. + if mf != nil && !mf.Config.MediaType.IsConfig() { + artifactType = string(mf.Config.MediaType) + } + + // Do nothing for tags; I give up. + // + // We'd like to validate that the "Docker-Content-Digest" header matches what is returned by the registry, + // but so many registries implement this incorrectly that it's not worth checking. + // + // For reference: + // https://github.com/GoogleContainerTools/kaniko/issues/298 + + // Return all this info since we have to calculate it anyway. + desc := v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + ArtifactType: artifactType, + } + + return manifest, &desc, nil +} + +func (f *fetcher) headManifest(ref name.Reference, acceptable []types.MediaType) (*v1.Descriptor, error) { + u := f.url("manifests", ref.Identifier()) + req, err := http.NewRequest(http.MethodHead, u.String(), nil) + if err != nil { + return nil, err + } + accept := []string{} + for _, mt := range acceptable { + accept = append(accept, string(mt)) + } + req.Header.Set("Accept", strings.Join(accept, ",")) + + resp, err := f.Client.Do(req.WithContext(f.context)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + mth := resp.Header.Get("Content-Type") + if mth == "" { + return nil, fmt.Errorf("HEAD %s: response did not include Content-Type header", u.String()) + } + mediaType := types.MediaType(mth) + + size := resp.ContentLength + if size == -1 { + return nil, fmt.Errorf("GET %s: response did not include Content-Length header", u.String()) + } + + dh := resp.Header.Get("Docker-Content-Digest") + if dh == "" { + return nil, fmt.Errorf("HEAD %s: response did not include Docker-Content-Digest header", u.String()) + } + digest, err := v1.NewHash(dh) + if err != nil { + return nil, err + } + + // Validate the digest matches what we asked for, if pulling by digest. + if dgst, ok := ref.(name.Digest); ok { + if digest.String() != dgst.DigestStr() { + return nil, fmt.Errorf("manifest digest: %q does not match requested digest: %q for %q", digest, dgst.DigestStr(), f.Ref) + } + } + + // Return all this info since we have to calculate it anyway. + return &v1.Descriptor{ + Digest: digest, + Size: size, + MediaType: mediaType, + }, nil +} + +func (f *fetcher) fetchBlob(ctx context.Context, size int64, h v1.Hash) (io.ReadCloser, error) { + u := f.url("blobs", h.String()) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + resp, err := f.Client.Do(req.WithContext(ctx)) + if err != nil { + return nil, redact.Error(err) + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + resp.Body.Close() + return nil, err + } + + // Do whatever we can. + // If we have an expected size and Content-Length doesn't match, return an error. + // If we don't have an expected size and we do have a Content-Length, use Content-Length. + if hsize := resp.ContentLength; hsize != -1 { + if size == verify.SizeUnknown { + size = hsize + } else if hsize != size { + return nil, fmt.Errorf("GET %s: Content-Length header %d does not match expected size %d", u.String(), hsize, size) + } + } + + return verify.ReadCloser(resp.Body, size, h) +} + +func (f *fetcher) headBlob(h v1.Hash) (*http.Response, error) { + u := f.url("blobs", h.String()) + req, err := http.NewRequest(http.MethodHead, u.String(), nil) + if err != nil { + return nil, err + } + + resp, err := f.Client.Do(req.WithContext(f.context)) + if err != nil { + return nil, redact.Error(err) + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + resp.Body.Close() + return nil, err + } + + return resp, nil +} + +func (f *fetcher) blobExists(h v1.Hash) (bool, error) { + u := f.url("blobs", h.String()) + req, err := http.NewRequest(http.MethodHead, u.String(), nil) + if err != nil { + return false, err + } + + resp, err := f.Client.Do(req.WithContext(f.context)) + if err != nil { + return false, redact.Error(err) + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + +// If filter applied, filter out by artifactType. +// See https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers +func filterReferrersResponse(filter map[string]string, origIndex *v1.IndexManifest) *v1.IndexManifest { + newIndex := origIndex + if filter == nil { + return newIndex + } + if v, ok := filter["artifactType"]; ok { + tmp := []v1.Descriptor{} + for _, desc := range newIndex.Manifests { + if desc.ArtifactType == v { + tmp = append(tmp, desc) + } + } + newIndex.Manifests = tmp + } + return newIndex +} diff --git a/pkg/v1/remote/descriptor_test.go b/pkg/v1/remote/descriptor_test.go new file mode 100644 index 0000000..1b77f80 --- /dev/null +++ b/pkg/v1/remote/descriptor_test.go @@ -0,0 +1,259 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestGetSchema1(t *testing.T) { + expectedRepo := "foo/bar" + fakeDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Header().Set("Content-Type", string(types.DockerManifestSchema1Signed)) + w.Header().Set("Docker-Content-Digest", fakeDigest) + w.Write([]byte("doesn't matter")) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + + // Get should succeed even for invalid json. We don't parse the response. + desc, err := Get(tag) + if err != nil { + t.Fatalf("Get(%s) = %v", tag, err) + } + + if desc.Digest.String() != fakeDigest { + t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest) + } + + want := `unsupported MediaType: "application/vnd.docker.distribution.manifest.v1+prettyjws", see https://github.com/google/go-containerregistry/issues/377` + // Should fail based on media type. + if _, err := desc.Image(); err != nil { + if errors.Is(err, &ErrSchema1{}) { + t.Errorf("Image() = %v, expected remote.ErrSchema1", err) + } + if diff := cmp.Diff(want, err.Error()); diff != "" { + t.Errorf("Image() error message (-want +got) = %v", diff) + } + } else { + t.Errorf("Image() = %v, expected err", err) + } + + // Should fail based on media type. + if _, err := desc.ImageIndex(); err != nil { + var s1err ErrSchema1 + if errors.Is(err, &s1err) { + t.Errorf("ImageImage() = %v, expected remote.ErrSchema1", err) + } + } else { + t.Errorf("ImageIndex() = %v, expected err", err) + } +} + +func TestGetImageAsIndex(t *testing.T) { + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Header().Set("Content-Type", string(types.DockerManifestSchema2)) + w.Write([]byte("doesn't matter")) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + + // Get should succeed even for invalid json. We don't parse the response. + desc, err := Get(tag) + if err != nil { + t.Fatalf("Get(%s) = %v", tag, err) + } + + // Should fail based on media type. + if _, err := desc.ImageIndex(); err == nil { + t.Errorf("ImageIndex() = %v, expected err", err) + } +} + +func TestHeadSchema1(t *testing.T) { + expectedRepo := "foo/bar" + mediaType := types.DockerManifestSchema1Signed + fakeDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000" + response := []byte("doesn't matter") + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodHead { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) + } + w.Header().Set("Content-Type", string(mediaType)) + w.Header().Set("Content-Length", strconv.Itoa(len(response))) + w.Header().Set("Docker-Content-Digest", fakeDigest) + w.Write(response) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + + // Head should succeed even for invalid json. We don't parse the response. + desc, err := Head(tag) + if err != nil { + t.Fatalf("Head(%s) = %v", tag, err) + } + + if desc.MediaType != mediaType { + t.Errorf("Descriptor.MediaType = %q, expected %q", desc.MediaType, mediaType) + } + + if desc.Digest.String() != fakeDigest { + t.Errorf("Descriptor.Digest = %q, expected %q", desc.Digest, fakeDigest) + } + + if desc.Size != int64(len(response)) { + t.Errorf("Descriptor.Size = %q, expected %q", desc.Size, len(response)) + } +} + +// TestHead_MissingHeaders tests that HEAD responses missing necessary headers +// result in errors. +func TestHead_MissingHeaders(t *testing.T) { + missingType := "missing-type" + missingLength := "missing-length" + missingDigest := "missing-digest" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v2/" { + w.WriteHeader(http.StatusOK) + return + } + if r.Method != http.MethodHead { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) + } + if !strings.Contains(r.URL.Path, missingType) { + w.Header().Set("Content-Type", "My-Media-Type") + } + if !strings.Contains(r.URL.Path, missingLength) { + w.Header().Set("Content-Length", "10") + } + if !strings.Contains(r.URL.Path, missingDigest) { + w.Header().Set("Docker-Content-Digest", "sha256:0000000000000000000000000000000000000000000000000000000000000000") + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + for _, repo := range []string{missingType, missingLength, missingDigest} { + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, repo)) + if _, err := Head(tag); err == nil { + t.Errorf("Head(%q): expected error, got nil", tag) + } + } +} + +// TestRedactFetchBlob tests that a request to fetchBlob that gets redirected +// to a URL that contains sensitive information has that information redacted +// if the subsequent request fails. +func TestRedactFetchBlob(t *testing.T) { + ctx := context.Background() + f := fetcher{ + Ref: mustNewTag(t, "original.com/repo:latest"), + Client: &http.Client{ + Transport: errTransport{}, + }, + context: ctx, + } + h, err := v1.NewHash("sha256:0000000000000000000000000000000000000000000000000000000000000000") + if err != nil { + t.Fatal("NewHash:", err) + } + if _, err := f.fetchBlob(ctx, 0, h); err == nil { + t.Fatalf("fetchBlob: expected error, got nil") + } else if !strings.Contains(err.Error(), "access_token=REDACTED") { + t.Fatalf("fetchBlob: expected error to contain redacted access token, got %v", err) + } +} + +type errTransport struct{} + +func (errTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // This simulates a registry that returns a redirect upon the first + // request, and then returns an error upon subsequent requests. This helps + // test whether error redaction takes into account URLs in error messasges + // that are not the original request URL. + if req.URL.Host == "original.com" { + return &http.Response{ + StatusCode: http.StatusSeeOther, + Header: http.Header{"Location": []string{"https://redirected.com?access_token=SECRET"}}, + }, nil + } + return nil, fmt.Errorf("error reaching %s", req.URL.String()) +} diff --git a/pkg/v1/remote/doc.go b/pkg/v1/remote/doc.go new file mode 100644 index 0000000..846ba07 --- /dev/null +++ b/pkg/v1/remote/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package remote provides facilities for reading/writing v1.Images from/to +// a remote image registry. +package remote diff --git a/pkg/v1/remote/error_roundtrip_test.go b/pkg/v1/remote/error_roundtrip_test.go new file mode 100644 index 0000000..5b81ee5 --- /dev/null +++ b/pkg/v1/remote/error_roundtrip_test.go @@ -0,0 +1,127 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote_test + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +func TestStatusCodeReturned(t *testing.T) { + tcs := []struct { + Description string + Handler http.Handler + }{{ + Description: "Only returns teapot status", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusTeapot) + }), + }, { + Description: "Handle v2, returns teapot status else", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Print(r.URL.Path) + if r.URL.Path == "/v2/" { + return + } + w.WriteHeader(http.StatusTeapot) + }), + }} + + for _, tc := range tcs { + t.Run(tc.Description, func(t *testing.T) { + o := httptest.NewServer(tc.Handler) + defer o.Close() + + ref, err := name.NewDigest(strings.TrimPrefix(o.URL+"/foo:@sha256:53b27244ffa2f585799adbfaf79fba5a5af104597751b289c8b235e7b8f7ebf5", "http://")) + + if err != nil { + t.Fatalf("Unable to parse digest: %v", err) + } + + _, err = remote.Image(ref) + var terr *transport.Error + if !errors.As(err, &terr) { + t.Fatalf("Unable to cast error to transport error: %v", err) + } + if terr.StatusCode != http.StatusTeapot { + t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot) + } + }) + } +} + +func TestBlobStatusCodeReturned(t *testing.T) { + reg := registry.New() + rh := httptest.NewServer(reg) + defer rh.Close() + i, _ := random.Image(1024, 16) + tag := strings.TrimPrefix(fmt.Sprintf("%s/foo:bar", rh.URL), "http://") + d, _ := name.NewTag(tag) + if err := remote.Write(d, i); err != nil { + t.Fatalf("Unable to write empty image: %v", err) + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Print(r.URL.Path) + if strings.Contains(r.URL.Path, "blob") { + w.WriteHeader(http.StatusTeapot) + return + } + reg.ServeHTTP(w, r) + }) + + o := httptest.NewServer(handler) + defer o.Close() + + ref, err := name.NewTag(strings.TrimPrefix(fmt.Sprintf("%s/foo:bar", o.URL), "http://")) + if err != nil { + t.Fatalf("Unable to parse digest: %v", err) + } + + ri, err := remote.Image(ref) + if err != nil { + t.Fatalf("Unable to fetch manifest: %v", err) + } + l, err := ri.Layers() + if err != nil { + t.Fatalf("Unable to fetch layers: %v", err) + } + _, err = l[0].Compressed() + var terr *transport.Error + if !errors.As(err, &terr) { + t.Fatalf("Unable to cast error to transport error: %v", err) + } + if terr.StatusCode != http.StatusTeapot { + t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot) + } + _, err = l[0].Uncompressed() + if !errors.As(err, &terr) { + t.Fatalf("Unable to cast error to transport error: %v", err) + } + if terr.StatusCode != http.StatusTeapot { + t.Errorf("Incorrect status code received, got %v, wanted %v", terr.StatusCode, http.StatusTeapot) + } +} diff --git a/pkg/v1/remote/image.go b/pkg/v1/remote/image.go new file mode 100644 index 0000000..fde6142 --- /dev/null +++ b/pkg/v1/remote/image.go @@ -0,0 +1,256 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "io" + "net/http" + "net/url" + "sync" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/internal/verify" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var acceptableImageMediaTypes = []types.MediaType{ + types.DockerManifestSchema2, + types.OCIManifestSchema1, +} + +// remoteImage accesses an image from a remote registry +type remoteImage struct { + fetcher + manifestLock sync.Mutex // Protects manifest + manifest []byte + configLock sync.Mutex // Protects config + config []byte + mediaType types.MediaType + descriptor *v1.Descriptor +} + +func (r *remoteImage) ArtifactType() (string, error) { + // kind of a hack, but RawManifest does appropriate locking/memoization + // and makes sure r.descriptor is populated. + if _, err := r.RawManifest(); err != nil { + return "", err + } + return r.descriptor.ArtifactType, nil +} + +var _ partial.CompressedImageCore = (*remoteImage)(nil) + +// Image provides access to a remote image reference. +func Image(ref name.Reference, options ...Option) (v1.Image, error) { + desc, err := Get(ref, options...) + if err != nil { + return nil, err + } + + return desc.Image() +} + +func (r *remoteImage) MediaType() (types.MediaType, error) { + if string(r.mediaType) != "" { + return r.mediaType, nil + } + return types.DockerManifestSchema2, nil +} + +func (r *remoteImage) RawManifest() ([]byte, error) { + r.manifestLock.Lock() + defer r.manifestLock.Unlock() + if r.manifest != nil { + return r.manifest, nil + } + + // NOTE(jonjohnsonjr): We should never get here because the public entrypoints + // do type-checking via remote.Descriptor. I've left this here for tests that + // directly instantiate a remoteImage. + manifest, desc, err := r.fetchManifest(r.Ref, acceptableImageMediaTypes) + if err != nil { + return nil, err + } + + if r.descriptor == nil { + r.descriptor = desc + } + r.mediaType = desc.MediaType + r.manifest = manifest + return r.manifest, nil +} + +func (r *remoteImage) RawConfigFile() ([]byte, error) { + r.configLock.Lock() + defer r.configLock.Unlock() + if r.config != nil { + return r.config, nil + } + + m, err := partial.Manifest(r) + if err != nil { + return nil, err + } + + if m.Config.Data != nil { + if err := verify.Descriptor(m.Config); err != nil { + return nil, err + } + r.config = m.Config.Data + return r.config, nil + } + + body, err := r.fetchBlob(r.context, m.Config.Size, m.Config.Digest) + if err != nil { + return nil, err + } + defer body.Close() + + r.config, err = io.ReadAll(body) + if err != nil { + return nil, err + } + return r.config, nil +} + +// Descriptor retains the original descriptor from an index manifest. +// See partial.Descriptor. +func (r *remoteImage) Descriptor() (*v1.Descriptor, error) { + // kind of a hack, but RawManifest does appropriate locking/memoization + // and makes sure r.descriptor is populated. + _, err := r.RawManifest() + return r.descriptor, err +} + +// remoteImageLayer implements partial.CompressedLayer +type remoteImageLayer struct { + ri *remoteImage + digest v1.Hash +} + +// Digest implements partial.CompressedLayer +func (rl *remoteImageLayer) Digest() (v1.Hash, error) { + return rl.digest, nil +} + +// Compressed implements partial.CompressedLayer +func (rl *remoteImageLayer) Compressed() (io.ReadCloser, error) { + urls := []url.URL{rl.ri.url("blobs", rl.digest.String())} + + // Add alternative layer sources from URLs (usually none). + d, err := partial.BlobDescriptor(rl, rl.digest) + if err != nil { + return nil, err + } + + if d.Data != nil { + return verify.ReadCloser(io.NopCloser(bytes.NewReader(d.Data)), d.Size, d.Digest) + } + + // We don't want to log binary layers -- this can break terminals. + ctx := redact.NewContext(rl.ri.context, "omitting binary blobs from logs") + + for _, s := range d.URLs { + u, err := url.Parse(s) + if err != nil { + return nil, err + } + urls = append(urls, *u) + } + + // The lastErr for most pulls will be the same (the first error), but for + // foreign layers we'll want to surface the last one, since we try to pull + // from the registry first, which would often fail. + // TODO: Maybe we don't want to try pulling from the registry first? + var lastErr error + for _, u := range urls { + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + resp, err := rl.ri.Client.Do(req.WithContext(ctx)) + if err != nil { + lastErr = err + continue + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + resp.Body.Close() + lastErr = err + continue + } + + return verify.ReadCloser(resp.Body, d.Size, rl.digest) + } + + return nil, lastErr +} + +// Manifest implements partial.WithManifest so that we can use partial.BlobSize below. +func (rl *remoteImageLayer) Manifest() (*v1.Manifest, error) { + return partial.Manifest(rl.ri) +} + +// MediaType implements v1.Layer +func (rl *remoteImageLayer) MediaType() (types.MediaType, error) { + bd, err := partial.BlobDescriptor(rl, rl.digest) + if err != nil { + return "", err + } + + return bd.MediaType, nil +} + +// Size implements partial.CompressedLayer +func (rl *remoteImageLayer) Size() (int64, error) { + // Look up the size of this digest in the manifest to avoid a request. + return partial.BlobSize(rl, rl.digest) +} + +// ConfigFile implements partial.WithManifestAndConfigFile so that we can use partial.BlobToDiffID below. +func (rl *remoteImageLayer) ConfigFile() (*v1.ConfigFile, error) { + return partial.ConfigFile(rl.ri) +} + +// DiffID implements partial.WithDiffID so that we don't recompute a DiffID that we already have +// available in our ConfigFile. +func (rl *remoteImageLayer) DiffID() (v1.Hash, error) { + return partial.BlobToDiffID(rl, rl.digest) +} + +// Descriptor retains the original descriptor from an image manifest. +// See partial.Descriptor. +func (rl *remoteImageLayer) Descriptor() (*v1.Descriptor, error) { + return partial.BlobDescriptor(rl, rl.digest) +} + +// See partial.Exists. +func (rl *remoteImageLayer) Exists() (bool, error) { + return rl.ri.blobExists(rl.digest) +} + +// LayerByDigest implements partial.CompressedLayer +func (r *remoteImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) { + return &remoteImageLayer{ + ri: r, + digest: h, + }, nil +} diff --git a/pkg/v1/remote/image_test.go b/pkg/v1/remote/image_test.go new file mode 100644 index 0000000..4a6c29d --- /dev/null +++ b/pkg/v1/remote/image_test.go @@ -0,0 +1,743 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "path" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +const bogusDigest = "sha256:deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + +type withDigest interface { + Digest() (v1.Hash, error) +} + +func mustDigest(t *testing.T, img withDigest) v1.Hash { + h, err := img.Digest() + if err != nil { + t.Fatalf("Digest() = %v", err) + } + return h +} + +func mustManifest(t *testing.T, img v1.Image) *v1.Manifest { + m, err := img.Manifest() + if err != nil { + t.Fatalf("Manifest() = %v", err) + } + return m +} + +func mustRawManifest(t *testing.T, img Taggable) []byte { + m, err := img.RawManifest() + if err != nil { + t.Fatalf("RawManifest() = %v", err) + } + return m +} + +func mustRawConfigFile(t *testing.T, img v1.Image) []byte { + c, err := img.RawConfigFile() + if err != nil { + t.Fatalf("RawConfigFile() = %v", err) + } + return c +} + +func randomImage(t *testing.T) v1.Image { + rnd, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + return rnd +} + +func newReference(host, repo, ref string) (name.Reference, error) { + tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", host, repo, ref), name.WeakValidation) + if err == nil { + return tag, nil + } + return name.NewDigest(fmt.Sprintf("%s/%s@%s", host, repo, ref), name.WeakValidation) +} + +// TODO(jonjohnsonjr): Make this real. +func TestMediaType(t *testing.T) { + img := remoteImage{} + got, err := img.MediaType() + if err != nil { + t.Fatalf("MediaType() = %v", err) + } + want := types.DockerManifestSchema2 + if got != want { + t.Errorf("MediaType() = %v, want %v", got, want) + } +} + +func TestRawManifestDigests(t *testing.T) { + img := randomImage(t) + expectedRepo := "foo/bar" + + cases := []struct { + name string + ref string + responseBody []byte + contentDigest string + wantErr bool + }{{ + name: "normal pull, by tag", + ref: "latest", + responseBody: mustRawManifest(t, img), + contentDigest: mustDigest(t, img).String(), + wantErr: false, + }, { + name: "normal pull, by digest", + ref: mustDigest(t, img).String(), + responseBody: mustRawManifest(t, img), + contentDigest: mustDigest(t, img).String(), + wantErr: false, + }, { + name: "right content-digest, wrong body, by digest", + ref: mustDigest(t, img).String(), + responseBody: []byte("not even json"), + contentDigest: mustDigest(t, img).String(), + wantErr: true, + }, { + name: "right body, wrong content-digest, by tag", + ref: "latest", + responseBody: mustRawManifest(t, img), + contentDigest: bogusDigest, + wantErr: false, + }, { + // NB: This succeeds! We don't care what the registry thinks. + name: "right body, wrong content-digest, by digest", + ref: mustDigest(t, img).String(), + responseBody: mustRawManifest(t, img), + contentDigest: bogusDigest, + wantErr: false, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + manifestPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, tc.ref) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Header().Set("Docker-Content-Digest", tc.contentDigest) + w.Write(tc.responseBody) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + ref, err := newReference(u.Host, expectedRepo, tc.ref) + if err != nil { + t.Fatalf("url.Parse(%v, %v, %v) = %v", u.Host, expectedRepo, tc.ref, err) + } + + rmt := remoteImage{ + fetcher: fetcher{ + Ref: ref, + Client: http.DefaultClient, + context: context.Background(), + }, + } + + if _, err := rmt.RawManifest(); (err != nil) != tc.wantErr { + t.Errorf("RawManifest() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + }) + } +} + +func TestRawManifestNotFound(t *testing.T) { + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.WriteHeader(http.StatusNotFound) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + img := remoteImage{ + fetcher: fetcher{ + Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)), + Client: http.DefaultClient, + context: context.Background(), + }, + } + + if _, err := img.RawManifest(); err == nil { + t.Error("RawManifest() = nil; wanted error") + } +} + +func TestRawConfigFileNotFound(t *testing.T) { + img := randomImage(t) + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img)) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.WriteHeader(http.StatusNotFound) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, img)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + rmt := remoteImage{ + fetcher: fetcher{ + Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)), + Client: http.DefaultClient, + context: context.Background(), + }, + } + + if _, err := rmt.RawConfigFile(); err == nil { + t.Error("RawConfigFile() = nil; wanted error") + } +} + +func TestAcceptHeaders(t *testing.T) { + img := randomImage(t) + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + wantAccept := strings.Join([]string{ + string(types.DockerManifestSchema2), + string(types.OCIManifestSchema1), + }, ",") + if got, want := r.Header.Get("Accept"), wantAccept; got != want { + t.Errorf("Accept header; got %v, want %v", got, want) + } + w.Write(mustRawManifest(t, img)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + rmt := &remoteImage{ + fetcher: fetcher{ + Ref: mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)), + Client: http.DefaultClient, + context: context.Background(), + }, + } + manifest, err := rmt.RawManifest() + if err != nil { + t.Errorf("RawManifest() = %v", err) + } + if got, want := manifest, mustRawManifest(t, img); !bytes.Equal(got, want) { + t.Errorf("RawManifest() = %v, want %v", got, want) + } +} + +func TestImage(t *testing.T) { + img := randomImage(t) + expectedRepo := "foo/bar" + layerDigest := mustManifest(t, img).Layers[0].Digest + layerSize := mustManifest(t, img).Layers[0].Size + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img)) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + layerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, layerDigest) + manifestReqCount := 0 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawConfigFile(t, img)) + case manifestPath: + manifestReqCount++ + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, img)) + case layerPath: + t.Fatalf("BlobSize should not make any request: %v", r.URL.Path) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + rmt, err := Image(tag, WithTransport(http.DefaultTransport), WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + t.Errorf("Image() = %v", err) + } + + if got, want := mustRawManifest(t, rmt), mustRawManifest(t, img); !bytes.Equal(got, want) { + t.Errorf("RawManifest() = %v, want %v", got, want) + } + if got, want := mustRawConfigFile(t, rmt), mustRawConfigFile(t, img); !bytes.Equal(got, want) { + t.Errorf("RawConfigFile() = %v, want %v", got, want) + } + // Make sure caching the manifest works. + if manifestReqCount != 1 { + t.Errorf("RawManifest made %v requests, expected 1", manifestReqCount) + } + + l, err := rmt.LayerByDigest(layerDigest) + if err != nil { + t.Errorf("LayerByDigest() = %v", err) + } + // BlobSize should not HEAD. + size, err := l.Size() + if err != nil { + t.Errorf("BlobSize() = %v", err) + } + if got, want := size, layerSize; want != got { + t.Errorf("BlobSize() = %v want %v", got, want) + } +} + +func TestPullingManifestList(t *testing.T) { + idx := randomIndex(t) + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + childDigest := mustIndexManifest(t, idx).Manifests[1].Digest + child := mustChild(t, idx, childDigest) + childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest) + fakePlatformChildDigest := mustIndexManifest(t, idx).Manifests[0].Digest + fakePlatformChild := mustChild(t, idx, fakePlatformChildDigest) + fakePlatformChildPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, fakePlatformChildDigest) + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child)) + + fakePlatform := v1.Platform{ + Architecture: "not-real-arch", + OS: "not-real-os", + } + + // Rewrite the index to make sure the desired platform matches the second child. + manifest, err := idx.IndexManifest() + if err != nil { + t.Fatal(err) + } + // Make sure the first manifest doesn't match. + manifest.Manifests[0].Platform = &fakePlatform + // Make sure the second manifest does. + manifest.Manifests[1].Platform = &defaultPlatform + // Do short-circuiting via Data. + manifest.Manifests[1].Data = mustRawManifest(t, child) + rawManifest, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Header().Set("Content-Type", string(mustMediaType(t, idx))) + w.Write(rawManifest) + case childPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, child)) + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawConfigFile(t, child)) + case fakePlatformChildPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, fakePlatformChild)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + rmtChild, err := Image(tag) + if err != nil { + t.Errorf("Image() = %v", err) + } + + // Test that child works as expected. + if got, want := mustRawManifest(t, rmtChild), mustRawManifest(t, child); !bytes.Equal(got, want) { + t.Errorf("RawManifest() = %v, want %v", string(got), string(want)) + } + if got, want := mustRawConfigFile(t, rmtChild), mustRawConfigFile(t, child); !bytes.Equal(got, want) { + t.Errorf("RawConfigFile() = %v, want %v", got, want) + } + + // Make sure we can roundtrip platform info via Descriptor. + img, err := Image(tag, WithPlatform(fakePlatform)) + if err != nil { + t.Fatal(err) + } + desc, err := partial.Descriptor(img) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(*desc.Platform, fakePlatform); diff != "" { + t.Errorf("Desciptor() (-want +got) = %v", diff) + } +} + +func TestPullingManifestListNoMatch(t *testing.T) { + idx := randomIndex(t) + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + childDigest := mustIndexManifest(t, idx).Manifests[1].Digest + child := mustChild(t, idx, childDigest) + childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest) + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child)) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Header().Set("Content-Type", string(mustMediaType(t, idx))) + w.Write(mustRawManifest(t, idx)) + case childPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, child)) + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawConfigFile(t, child)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + platform := v1.Platform{ + Architecture: "not-real-arch", + OS: "not-real-os", + } + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + if _, err := Image(tag, WithPlatform(platform)); err == nil { + t.Errorf("Image succeeded, wanted err") + } +} + +func TestValidate(t *testing.T) { + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tag, err := name.NewTag(u.Host + "/foo/bar") + if err != nil { + t.Fatal(err) + } + + if err := Write(tag, img); err != nil { + t.Fatal(err) + } + + img, err = Image(tag) + if err != nil { + t.Fatal(err) + } + + if err := validate.Image(img); err != nil { + t.Errorf("failed to validate remote.Image: %v", err) + } +} + +func TestPullingForeignLayer(t *testing.T) { + // For that sweet, sweet coverage in options. + var b bytes.Buffer + logs.Debug.SetOutput(&b) + + img := randomImage(t) + expectedRepo := "foo/bar" + foreignPath := "/foreign/path" + + foreignLayer, err := random.Layer(1024, types.DockerForeignLayer) + if err != nil { + t.Fatal(err) + } + + foreignServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case foreignPath: + compressed, err := foreignLayer.Compressed() + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(w, compressed); err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusOK) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer foreignServer.Close() + fu, err := url.Parse(foreignServer.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", foreignServer.URL, err) + } + + img, err = mutate.Append(img, mutate.Addendum{ + Layer: foreignLayer, + URLs: []string{ + "http://" + path.Join(fu.Host, foreignPath), + }, + }) + if err != nil { + t.Fatal(err) + } + + // Set up a fake registry that will respond 404 to the foreign layer, + // but serve everything else correctly. + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, img)) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + foreignLayerDigest := mustManifest(t, img).Layers[1].Digest + foreignLayerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, foreignLayerDigest) + layerDigest := mustManifest(t, img).Layers[0].Digest + layerPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, layerDigest) + + layer, err := img.LayerByDigest(layerDigest) + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawConfigFile(t, img)) + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, img)) + case layerPath: + compressed, err := layer.Compressed() + if err != nil { + t.Fatal(err) + } + if _, err := io.Copy(w, compressed); err != nil { + t.Fatal(err) + } + w.WriteHeader(http.StatusOK) + case foreignLayerPath: + // Not here! + w.WriteHeader(http.StatusNotFound) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + // Pull from the registry and ensure that everything Validates; i.e. that + // we pull the layer from the foreignServer. + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + rmt, err := Image(tag, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("Image() = %v", err) + } + + if err := validate.Image(rmt); err != nil { + t.Errorf("failed to validate foreign image: %v", err) + } + + // Set up a fake registry and write what we pulled to it. + // This ensures we get coverage for the remoteLayer.MediaType path. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err = url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/foreign/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, rmt); err != nil { + t.Errorf("failed to Write: %v", err) + } +} + +func TestData(t *testing.T) { + img := randomImage(t) + manifest, err := img.Manifest() + if err != nil { + t.Fatal(err) + } + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + cb, err := img.RawConfigFile() + if err != nil { + t.Fatal(err) + } + + manifest.Config.Data = cb + rc, err := layers[0].Compressed() + if err != nil { + t.Fatal(err) + } + lb, err := io.ReadAll(rc) + if err != nil { + t.Fatal(err) + } + manifest.Layers[0].Data = lb + rawManifest, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case "/v2/test/manifests/latest": + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(rawManifest) + default: + // explode if we try to read blob or config + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + ref, err := newReference(u.Host, "test", "latest") + if err != nil { + t.Fatal(err) + } + rmt, err := Image(ref) + if err != nil { + t.Fatal(err) + } + if err := validate.Image(rmt); err != nil { + t.Fatal(err) + } +} diff --git a/pkg/v1/remote/index.go b/pkg/v1/remote/index.go new file mode 100644 index 0000000..0939947 --- /dev/null +++ b/pkg/v1/remote/index.go @@ -0,0 +1,319 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "fmt" + "sync" + + "github.com/google/go-containerregistry/internal/verify" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var acceptableIndexMediaTypes = []types.MediaType{ + types.DockerManifestList, + types.OCIImageIndex, +} + +// remoteIndex accesses an index from a remote registry +type remoteIndex struct { + fetcher + manifestLock sync.Mutex // Protects manifest + manifest []byte + mediaType types.MediaType + descriptor *v1.Descriptor +} + +// Index provides access to a remote index reference. +func Index(ref name.Reference, options ...Option) (v1.ImageIndex, error) { + desc, err := get(ref, acceptableIndexMediaTypes, options...) + if err != nil { + return nil, err + } + + return desc.ImageIndex() +} + +func (r *remoteIndex) MediaType() (types.MediaType, error) { + if string(r.mediaType) != "" { + return r.mediaType, nil + } + return types.DockerManifestList, nil +} + +func (r *remoteIndex) Digest() (v1.Hash, error) { + return partial.Digest(r) +} + +func (r *remoteIndex) Size() (int64, error) { + return partial.Size(r) +} + +func (r *remoteIndex) RawManifest() ([]byte, error) { + r.manifestLock.Lock() + defer r.manifestLock.Unlock() + if r.manifest != nil { + return r.manifest, nil + } + + // NOTE(jonjohnsonjr): We should never get here because the public entrypoints + // do type-checking via remote.Descriptor. I've left this here for tests that + // directly instantiate a remoteIndex. + manifest, desc, err := r.fetchManifest(r.Ref, acceptableIndexMediaTypes) + if err != nil { + return nil, err + } + + if r.descriptor == nil { + r.descriptor = desc + } + r.mediaType = desc.MediaType + r.manifest = manifest + return r.manifest, nil +} + +func (r *remoteIndex) IndexManifest() (*v1.IndexManifest, error) { + b, err := r.RawManifest() + if err != nil { + return nil, err + } + return v1.ParseIndexManifest(bytes.NewReader(b)) +} + +func (r *remoteIndex) Image(h v1.Hash) (v1.Image, error) { + desc, err := r.childByHash(h) + if err != nil { + return nil, err + } + + // Descriptor.Image will handle coercing nested indexes into an Image. + return desc.Image() +} + +// Descriptor retains the original descriptor from an index manifest. +// See partial.Descriptor. +func (r *remoteIndex) Descriptor() (*v1.Descriptor, error) { + // kind of a hack, but RawManifest does appropriate locking/memoization + // and makes sure r.descriptor is populated. + _, err := r.RawManifest() + return r.descriptor, err +} + +func (r *remoteIndex) ImageIndex(h v1.Hash) (v1.ImageIndex, error) { + desc, err := r.childByHash(h) + if err != nil { + return nil, err + } + return desc.ImageIndex() +} + +// Workaround for #819. +func (r *remoteIndex) Layer(h v1.Hash) (v1.Layer, error) { + index, err := r.IndexManifest() + if err != nil { + return nil, err + } + for _, childDesc := range index.Manifests { + if h == childDesc.Digest { + l, err := partial.CompressedToLayer(&remoteLayer{ + fetcher: r.fetcher, + digest: h, + }) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: r.Ref.Context().Digest(h.String()), + }, nil + } + } + return nil, fmt.Errorf("layer not found: %s", h) +} + +// Experiment with a better API for v1.ImageIndex. We might want to move this +// to partial? +func (r *remoteIndex) Manifests() ([]partial.Describable, error) { + m, err := r.IndexManifest() + if err != nil { + return nil, err + } + manifests := []partial.Describable{} + for _, desc := range m.Manifests { + switch { + case desc.MediaType.IsImage(): + img, err := r.Image(desc.Digest) + if err != nil { + return nil, err + } + manifests = append(manifests, img) + case desc.MediaType.IsIndex(): + idx, err := r.ImageIndex(desc.Digest) + if err != nil { + return nil, err + } + manifests = append(manifests, idx) + default: + layer, err := r.Layer(desc.Digest) + if err != nil { + return nil, err + } + manifests = append(manifests, layer) + } + } + + return manifests, nil +} + +func (r *remoteIndex) imageByPlatform(platform v1.Platform) (v1.Image, error) { + desc, err := r.childByPlatform(platform) + if err != nil { + return nil, err + } + + // Descriptor.Image will handle coercing nested indexes into an Image. + return desc.Image() +} + +// This naively matches the first manifest with matching platform attributes. +// +// We should probably use this instead: +// +// github.com/containerd/containerd/platforms +// +// But first we'd need to migrate to: +// +// github.com/opencontainers/image-spec/specs-go/v1 +func (r *remoteIndex) childByPlatform(platform v1.Platform) (*Descriptor, error) { + index, err := r.IndexManifest() + if err != nil { + return nil, err + } + for _, childDesc := range index.Manifests { + // If platform is missing from child descriptor, assume it's amd64/linux. + p := defaultPlatform + if childDesc.Platform != nil { + p = *childDesc.Platform + } + + if matchesPlatform(p, platform) { + return r.childDescriptor(childDesc, platform) + } + } + return nil, fmt.Errorf("no child with platform %+v in index %s", platform, r.Ref) +} + +func (r *remoteIndex) childByHash(h v1.Hash) (*Descriptor, error) { + index, err := r.IndexManifest() + if err != nil { + return nil, err + } + for _, childDesc := range index.Manifests { + if h == childDesc.Digest { + return r.childDescriptor(childDesc, defaultPlatform) + } + } + return nil, fmt.Errorf("no child with digest %s in index %s", h, r.Ref) +} + +// Convert one of this index's child's v1.Descriptor into a remote.Descriptor, with the given platform option. +func (r *remoteIndex) childDescriptor(child v1.Descriptor, platform v1.Platform) (*Descriptor, error) { + ref := r.Ref.Context().Digest(child.Digest.String()) + var ( + manifest []byte + err error + ) + if child.Data != nil { + if err := verify.Descriptor(child); err != nil { + return nil, err + } + manifest = child.Data + } else { + manifest, _, err = r.fetchManifest(ref, []types.MediaType{child.MediaType}) + if err != nil { + return nil, err + } + } + + if child.MediaType.IsImage() { + mf, _ := v1.ParseManifest(bytes.NewReader(manifest)) + // Failing to parse as a manifest should just be ignored. + // The manifest might not be valid, and that's okay. + if mf != nil && !mf.Config.MediaType.IsConfig() { + child.ArtifactType = string(mf.Config.MediaType) + } + } + + return &Descriptor{ + fetcher: fetcher{ + Ref: ref, + Client: r.Client, + context: r.context, + }, + Manifest: manifest, + Descriptor: child, + platform: platform, + }, nil +} + +// matchesPlatform checks if the given platform matches the required platforms. +// The given platform matches the required platform if +// - architecture and OS are identical. +// - OS version and variant are identical if provided. +// - features and OS features of the required platform are subsets of those of the given platform. +func matchesPlatform(given, required v1.Platform) bool { + // Required fields that must be identical. + if given.Architecture != required.Architecture || given.OS != required.OS { + return false + } + + // Optional fields that may be empty, but must be identical if provided. + if required.OSVersion != "" && given.OSVersion != required.OSVersion { + return false + } + if required.Variant != "" && given.Variant != required.Variant { + return false + } + + // Verify required platform's features are a subset of given platform's features. + if !isSubset(given.OSFeatures, required.OSFeatures) { + return false + } + if !isSubset(given.Features, required.Features) { + return false + } + + return true +} + +// isSubset checks if the required array of strings is a subset of the given lst. +func isSubset(lst, required []string) bool { + set := make(map[string]bool) + for _, value := range lst { + set[value] = true + } + + for _, value := range required { + if _, ok := set[value]; !ok { + return false + } + } + + return true +} diff --git a/pkg/v1/remote/index_test.go b/pkg/v1/remote/index_test.go new file mode 100644 index 0000000..4399b16 --- /dev/null +++ b/pkg/v1/remote/index_test.go @@ -0,0 +1,504 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func randomIndex(t *testing.T) v1.ImageIndex { + rnd, err := random.Index(1024, 1, 3) + if err != nil { + t.Fatalf("random.Index() = %v", err) + } + return rnd +} + +func mustIndexManifest(t *testing.T, idx v1.ImageIndex) *v1.IndexManifest { + m, err := idx.IndexManifest() + if err != nil { + t.Fatalf("IndexManifest() = %v", err) + } + return m +} + +func mustChild(t *testing.T, idx v1.ImageIndex, h v1.Hash) v1.Image { + img, err := idx.Image(h) + if err != nil { + t.Fatalf("Image(%s) = %v", h, err) + } + return img +} + +func mustMediaType(t *testing.T, tag withMediaType) types.MediaType { + mt, err := tag.MediaType() + if err != nil { + t.Fatalf("MediaType() = %v", err) + } + return mt +} + +func mustHash(t *testing.T, s string) v1.Hash { + h, err := v1.NewHash(s) + if err != nil { + t.Fatalf("NewHash() = %v", err) + } + return h +} + +func TestIndexRawManifestDigests(t *testing.T) { + idx := randomIndex(t) + expectedRepo := "foo/bar" + + cases := []struct { + name string + ref string + responseBody []byte + contentDigest string + wantErr bool + }{{ + name: "normal pull, by tag", + ref: "latest", + responseBody: mustRawManifest(t, idx), + contentDigest: mustDigest(t, idx).String(), + wantErr: false, + }, { + name: "normal pull, by digest", + ref: mustDigest(t, idx).String(), + responseBody: mustRawManifest(t, idx), + contentDigest: mustDigest(t, idx).String(), + wantErr: false, + }, { + name: "right content-digest, wrong body, by digest", + ref: mustDigest(t, idx).String(), + responseBody: []byte("not even json"), + contentDigest: mustDigest(t, idx).String(), + wantErr: true, + }, { + name: "right body, wrong content-digest, by tag", + ref: "latest", + responseBody: mustRawManifest(t, idx), + contentDigest: bogusDigest, + wantErr: false, + }, { + // NB: This succeeds! We don't care what the registry thinks. + name: "right body, wrong content-digest, by digest", + ref: mustDigest(t, idx).String(), + responseBody: mustRawManifest(t, idx), + contentDigest: bogusDigest, + wantErr: false, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + manifestPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, tc.ref) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case manifestPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Header().Set("Docker-Content-Digest", tc.contentDigest) + w.Write(tc.responseBody) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + ref, err := newReference(u.Host, expectedRepo, tc.ref) + if err != nil { + t.Fatalf("url.Parse(%v, %v, %v) = %v", u.Host, expectedRepo, tc.ref, err) + } + + rmt := remoteIndex{ + fetcher: fetcher{ + Ref: ref, + Client: http.DefaultClient, + context: context.Background(), + }, + } + + if _, err := rmt.RawManifest(); (err != nil) != tc.wantErr { + t.Errorf("RawManifest() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + }) + } +} + +func TestIndex(t *testing.T) { + idx := randomIndex(t) + expectedRepo := "foo/bar" + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + childDigest := mustIndexManifest(t, idx).Manifests[0].Digest + child := mustChild(t, idx, childDigest) + childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest) + configPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, mustConfigName(t, child)) + manifestReqCount := 0 + childReqCount := 0 + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case manifestPath: + manifestReqCount++ + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Header().Set("Content-Type", string(mustMediaType(t, idx))) + w.Write(mustRawManifest(t, idx)) + case childPath: + childReqCount++ + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawManifest(t, child)) + case configPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + w.Write(mustRawConfigFile(t, child)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + tag := mustNewTag(t, fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo)) + rmt, err := Index(tag, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("Index() = %v", err) + } + rmtChild, err := rmt.Image(childDigest) + if err != nil { + t.Errorf("remoteIndex.Image(%s) = %v", childDigest, err) + } + + // Test that index works as expected. + if got, want := mustRawManifest(t, rmt), mustRawManifest(t, idx); !bytes.Equal(got, want) { + t.Errorf("RawManifest() = %v, want %v", got, want) + } + if diff := cmp.Diff(mustIndexManifest(t, idx), mustIndexManifest(t, rmt)); diff != "" { + t.Errorf("IndexManifest() (-want +got) = %v", diff) + } + if got, want := mustMediaType(t, rmt), mustMediaType(t, idx); got != want { + t.Errorf("MediaType() = %v, want %v", got, want) + } + if got, want := mustDigest(t, rmt), mustDigest(t, idx); got != want { + t.Errorf("Digest() = %v, want %v", got, want) + } + // Make sure caching the manifest works for index. + if manifestReqCount != 1 { + t.Errorf("RawManifest made %v requests, expected 1", manifestReqCount) + } + + // Test that child works as expected. + if got, want := mustRawManifest(t, rmtChild), mustRawManifest(t, child); !bytes.Equal(got, want) { + t.Errorf("RawManifest() = %v, want %v", got, want) + } + if got, want := mustRawConfigFile(t, rmtChild), mustRawConfigFile(t, child); !bytes.Equal(got, want) { + t.Errorf("RawConfigFile() = %v, want %v", got, want) + } + // Make sure caching the manifest works for child. + if childReqCount != 1 { + t.Errorf("RawManifest made %v requests, expected 1", childReqCount) + } + + // Try to fetch bogus children. + bogusHash := mustHash(t, bogusDigest) + + if _, err := rmt.Image(bogusHash); err == nil { + t.Errorf("remoteIndex.Image(bogusDigest) err = %v, wanted err", err) + } + if _, err := rmt.ImageIndex(bogusHash); err == nil { + t.Errorf("remoteIndex.ImageIndex(bogusDigest) err = %v, wanted err", err) + } +} + +// TestMatchesPlatform runs test cases on the matchesPlatform function which verifies +// whether the given platform can run on the required platform by checking the +// compatibility of architecture, OS, OS version, OS features, variant and features. +func TestMatchesPlatform(t *testing.T) { + t.Parallel() + tests := []struct { + // want is the expected return value from matchesPlatform + // when the given platform is 'given' and the required platform is 'required'. + given v1.Platform + required v1.Platform + want bool + }{{ // The given & required platforms are identical. matchesPlatform expected to return true. + given: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: true, + }, + { // OS and Architecture must exactly match. matchesPlatform expected to return false. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // OS version must exactly match + given: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10587", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // OS Features must exactly match. matchesPlatform expected to return false. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // Variant must exactly match. matchesPlatform expected to return false. + given: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv7l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // OS must exactly match, and is case sensative. matchesPlatform expected to return false. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "LinuX", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // OSVersion and Variant are specified in given but not in required. + // matchesPlatform expected to return true. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "", + OSFeatures: []string{"win64k"}, + Variant: "", + Features: []string{"sse4"}, + }, + want: true, + }, + { // Ensure the optional field OSVersion & Variant match exactly if specified as required. + given: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "", + OSFeatures: []string{}, + Variant: "", + Features: []string{}, + }, + required: v1.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: false, + }, + { // Checking subset validity when required less features than given features. + // matchesPlatform expected to return true. + given: v1.Platform{ + Architecture: "", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win32k"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "", + OS: "linux", + OSVersion: "", + OSFeatures: []string{}, + Variant: "", + Features: []string{}, + }, + want: true, + }, + { // Checking subset validity when required features are subset of given features. + // matchesPlatform expected to return true. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k", "f1", "f2"}, + Variant: "", + Features: []string{"sse4", "f1"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "", + Features: []string{"sse4"}, + }, + want: true, + }, + { // Checking subset validity when some required features is not subset of given features. + // matchesPlatform expected to return false. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k", "f1", "f2"}, + Variant: "", + Features: []string{"sse4", "f1"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k"}, + Variant: "", + Features: []string{"sse4", "f2"}, + }, + want: false, + }, + { // Checking subset validity when OS features not required, + // and required features is indeed a subset of given features. + // matchesPlatform expected to return true. + given: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{"win64k", "f1", "f2"}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + required: v1.Platform{ + Architecture: "arm", + OS: "linux", + OSVersion: "10.0.10586", + OSFeatures: []string{}, + Variant: "armv6l", + Features: []string{"sse4"}, + }, + want: true, + }, + } + + for _, test := range tests { + got := matchesPlatform(test.given, test.required) + if got != test.want { + t.Errorf("matchesPlatform(%v, %v); got %v, want %v", test.given, test.required, got, test.want) + } + } +} diff --git a/pkg/v1/remote/layer.go b/pkg/v1/remote/layer.go new file mode 100644 index 0000000..b2126f5 --- /dev/null +++ b/pkg/v1/remote/layer.go @@ -0,0 +1,94 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "io" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/internal/verify" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// remoteImagelayer implements partial.CompressedLayer +type remoteLayer struct { + fetcher + digest v1.Hash +} + +// Compressed implements partial.CompressedLayer +func (rl *remoteLayer) Compressed() (io.ReadCloser, error) { + // We don't want to log binary layers -- this can break terminals. + ctx := redact.NewContext(rl.context, "omitting binary blobs from logs") + return rl.fetchBlob(ctx, verify.SizeUnknown, rl.digest) +} + +// Compressed implements partial.CompressedLayer +func (rl *remoteLayer) Size() (int64, error) { + resp, err := rl.headBlob(rl.digest) + if err != nil { + return -1, err + } + defer resp.Body.Close() + return resp.ContentLength, nil +} + +// Digest implements partial.CompressedLayer +func (rl *remoteLayer) Digest() (v1.Hash, error) { + return rl.digest, nil +} + +// MediaType implements v1.Layer +func (rl *remoteLayer) MediaType() (types.MediaType, error) { + return types.DockerLayer, nil +} + +// See partial.Exists. +func (rl *remoteLayer) Exists() (bool, error) { + return rl.blobExists(rl.digest) +} + +// Layer reads the given blob reference from a registry as a Layer. A blob +// reference here is just a punned name.Digest where the digest portion is the +// digest of the blob to be read and the repository portion is the repo where +// that blob lives. +func Layer(ref name.Digest, options ...Option) (v1.Layer, error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return nil, err + } + f, err := makeFetcher(ref, o) + if err != nil { + return nil, err + } + h, err := v1.NewHash(ref.Identifier()) + if err != nil { + return nil, err + } + l, err := partial.CompressedToLayer(&remoteLayer{ + fetcher: *f, + digest: h, + }) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: ref, + }, nil +} diff --git a/pkg/v1/remote/layer_test.go b/pkg/v1/remote/layer_test.go new file mode 100644 index 0000000..a2f56bd --- /dev/null +++ b/pkg/v1/remote/layer_test.go @@ -0,0 +1,148 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "fmt" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestRemoteLayer(t *testing.T) { + layer, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + digest, err := layer.Digest() + if err != nil { + t.Fatal(err) + } + + // Set up a fake registry and write what we pulled to it. + // This ensures we get coverage for the remoteLayer.MediaType path. + s := httptest.NewServer(registry.New()) + defer s.Close() + t.Log(s.URL) + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + t.Log(u) + dst := fmt.Sprintf("%s/some/path@%s", u.Host, digest) + t.Log(dst) + ref, err := name.NewDigest(dst) + if err != nil { + t.Fatal(err) + } + + t.Log(ref) + if err := WriteLayer(ref.Context(), layer); err != nil { + t.Fatalf("failed to WriteLayer: %v", err) + } + + got, err := Layer(ref) + if err != nil { + t.Fatal(err) + } + + if _, err := got.MediaType(); err != nil { + t.Errorf("reading MediaType: %v", err) + } + + if err := compare.Layers(got, layer); err != nil { + t.Errorf("compare.Layers: %v", err) + } + if err := validate.Layer(got); err != nil { + t.Errorf("validate.Layer: %v", err) + } + + if ok, err := partial.Exists(got); err != nil { + t.Fatal(err) + } else if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } +} + +func TestRemoteLayerDescriptor(t *testing.T) { + layer, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + image, err := mutate.Append(empty.Image, mutate.Addendum{ + Layer: layer, + URLs: []string{"example.com"}, + }) + if err != nil { + t.Fatal(err) + } + + // Set up a fake registry and write what we pulled to it. + // This ensures we get coverage for the remoteLayer.MediaType path. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + dst := fmt.Sprintf("%s/some/path:tag", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, image); err != nil { + t.Fatalf("failed to WriteLayer: %v", err) + } + + pulled, err := Image(ref) + if err != nil { + t.Fatal(err) + } + + layers, err := pulled.Layers() + if err != nil { + t.Fatal(err) + } + + desc, err := partial.Descriptor(layers[0]) + if err != nil { + t.Fatal(err) + } + + if len(desc.URLs) != 1 { + t.Fatalf("expected url for layer[0]") + } + + if got, want := desc.URLs[0], "example.com"; got != want { + t.Errorf("layer[0].urls[0] = %s != %s", got, want) + } + if ok, err := partial.Exists(layers[0]); err != nil { + t.Fatal(err) + } else if got, want := ok, true; got != want { + t.Errorf("Exists() = %t != %t", got, want) + } +} diff --git a/pkg/v1/remote/list.go b/pkg/v1/remote/list.go new file mode 100644 index 0000000..e643c49 --- /dev/null +++ b/pkg/v1/remote/list.go @@ -0,0 +1,141 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +type tags struct { + Name string `json:"name"` + Tags []string `json:"tags"` +} + +// ListWithContext calls List with the given context. +// +// Deprecated: Use List and WithContext. This will be removed in a future release. +func ListWithContext(ctx context.Context, repo name.Repository, options ...Option) ([]string, error) { + return List(repo, append(options, WithContext(ctx))...) +} + +// List calls /tags/list for the given repository, returning the list of tags +// in the "tags" property. +func List(repo name.Repository, options ...Option) ([]string, error) { + o, err := makeOptions(repo, options...) + if err != nil { + return nil, err + } + scopes := []string{repo.Scope(transport.PullScope)} + tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes) + if err != nil { + return nil, err + } + + uri := &url.URL{ + Scheme: repo.Registry.Scheme(), + Host: repo.Registry.RegistryStr(), + Path: fmt.Sprintf("/v2/%s/tags/list", repo.RepositoryStr()), + } + + if o.pageSize > 0 { + uri.RawQuery = fmt.Sprintf("n=%d", o.pageSize) + } + + client := http.Client{Transport: tr} + tagList := []string{} + parsed := tags{} + + // get responses until there is no next page + for { + select { + case <-o.context.Done(): + return nil, o.context.Err() + default: + } + + req, err := http.NewRequestWithContext(o.context, "GET", uri.String(), nil) + if err != nil { + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if err := transport.CheckError(resp, http.StatusOK); err != nil { + return nil, err + } + + if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { + return nil, err + } + + if err := resp.Body.Close(); err != nil { + return nil, err + } + + tagList = append(tagList, parsed.Tags...) + + uri, err = getNextPageURL(resp) + if err != nil { + return nil, err + } + // no next page + if uri == nil { + break + } + } + + return tagList, nil +} + +// getNextPageURL checks if there is a Link header in a http.Response which +// contains a link to the next page. If yes it returns the url.URL of the next +// page otherwise it returns nil. +func getNextPageURL(resp *http.Response) (*url.URL, error) { + link := resp.Header.Get("Link") + if link == "" { + return nil, nil + } + + if link[0] != '<' { + return nil, fmt.Errorf("failed to parse link header: missing '<' in: %s", link) + } + + end := strings.Index(link, ">") + if end == -1 { + return nil, fmt.Errorf("failed to parse link header: missing '>' in: %s", link) + } + link = link[1:end] + + linkURL, err := url.Parse(link) + if err != nil { + return nil, err + } + if resp.Request == nil || resp.Request.URL == nil { + return nil, nil + } + linkURL = resp.Request.URL.ResolveReference(linkURL) + return linkURL, nil +} diff --git a/pkg/v1/remote/list_test.go b/pkg/v1/remote/list_test.go new file mode 100644 index 0000000..89700b8 --- /dev/null +++ b/pkg/v1/remote/list_test.go @@ -0,0 +1,159 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" +) + +func TestList(t *testing.T) { + cases := []struct { + name string + responseBody []byte + wantErr bool + wantTags []string + }{{ + name: "success", + responseBody: []byte(`{"tags":["foo","bar"]}`), + wantErr: false, + wantTags: []string{"foo", "bar"}, + }, { + name: "not json", + responseBody: []byte("notjson"), + wantErr: true, + }} + + repoName := "ubuntu" + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tagsPath := fmt.Sprintf("/v2/%s/tags/list", repoName) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case tagsPath: + if r.Method != http.MethodGet { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodGet) + } + + w.Write(tc.responseBody) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation) + if err != nil { + t.Fatalf("name.NewRepository(%v) = %v", repoName, err) + } + + tags, err := List(repo) + if (err != nil) != tc.wantErr { + t.Errorf("List() wrong error: %v, want %v: %v\n", (err != nil), tc.wantErr, err) + } + + if diff := cmp.Diff(tc.wantTags, tags); diff != "" { + t.Errorf("List() wrong tags (-want +got) = %s", diff) + } + }) + } +} + +func TestCancelledList(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + repoName := "doesnotmatter" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + + repo, err := name.NewRepository(fmt.Sprintf("%s/%s", u.Host, repoName), name.WeakValidation) + if err != nil { + t.Fatalf("name.NewRepository(%v) = %v", repoName, err) + } + + _, err = ListWithContext(ctx, repo) + if err == nil || !strings.Contains(err.Error(), "context canceled") { + t.Errorf(`unexpected error; want "context canceled", got %v`, err) + } +} + +func makeResp(hdr string) *http.Response { + return &http.Response{ + Header: http.Header{ + "Link": []string{hdr}, + }, + } +} + +func TestGetNextPageURL(t *testing.T) { + for _, hdr := range []string{ + "", + "<", + "><", + "<>", + fmt.Sprintf("<%c>", 0x7f), // makes url.Parse fail + } { + u, err := getNextPageURL(makeResp(hdr)) + if err == nil && u != nil { + t.Errorf("Expected err, got %+v", u) + } + } + + good := &http.Response{ + Header: http.Header{ + "Link": []string{"<example.com>"}, + }, + Request: &http.Request{ + URL: &url.URL{ + Scheme: "https", + }, + }, + } + u, err := getNextPageURL(good) + if err != nil { + t.Fatal(err) + } + + if u.Scheme != "https" { + t.Errorf("expected scheme to match request, got %s", u.Scheme) + } +} diff --git a/pkg/v1/remote/mount.go b/pkg/v1/remote/mount.go new file mode 100644 index 0000000..36d0885 --- /dev/null +++ b/pkg/v1/remote/mount.go @@ -0,0 +1,108 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" +) + +// MountableLayer wraps a v1.Layer in a shim that enables the layer to be +// "mounted" when published to another registry. +type MountableLayer struct { + v1.Layer + + Reference name.Reference +} + +// Descriptor retains the original descriptor from an image manifest. +// See partial.Descriptor. +func (ml *MountableLayer) Descriptor() (*v1.Descriptor, error) { + return partial.Descriptor(ml.Layer) +} + +// Exists is a hack. See partial.Exists. +func (ml *MountableLayer) Exists() (bool, error) { + return partial.Exists(ml.Layer) +} + +// mountableImage wraps the v1.Layer references returned by the embedded v1.Image +// in MountableLayer's so that remote.Write might attempt to mount them from their +// source repository. +type mountableImage struct { + v1.Image + + Reference name.Reference +} + +// Layers implements v1.Image +func (mi *mountableImage) Layers() ([]v1.Layer, error) { + ls, err := mi.Image.Layers() + if err != nil { + return nil, err + } + mls := make([]v1.Layer, 0, len(ls)) + for _, l := range ls { + mls = append(mls, &MountableLayer{ + Layer: l, + Reference: mi.Reference, + }) + } + return mls, nil +} + +// LayerByDigest implements v1.Image +func (mi *mountableImage) LayerByDigest(d v1.Hash) (v1.Layer, error) { + l, err := mi.Image.LayerByDigest(d) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: mi.Reference, + }, nil +} + +// LayerByDiffID implements v1.Image +func (mi *mountableImage) LayerByDiffID(d v1.Hash) (v1.Layer, error) { + l, err := mi.Image.LayerByDiffID(d) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: mi.Reference, + }, nil +} + +// Descriptor retains the original descriptor from an index manifest. +// See partial.Descriptor. +func (mi *mountableImage) Descriptor() (*v1.Descriptor, error) { + return partial.Descriptor(mi.Image) +} + +// ConfigLayer retains the original reference so that it can be mounted. +// See partial.ConfigLayer. +func (mi *mountableImage) ConfigLayer() (v1.Layer, error) { + l, err := partial.ConfigLayer(mi.Image) + if err != nil { + return nil, err + } + return &MountableLayer{ + Layer: l, + Reference: mi.Reference, + }, nil +} diff --git a/pkg/v1/remote/mount_test.go b/pkg/v1/remote/mount_test.go new file mode 100644 index 0000000..ad9b8f6 --- /dev/null +++ b/pkg/v1/remote/mount_test.go @@ -0,0 +1,55 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestMountableImage(t *testing.T) { + img, err := random.Image(1024, 5) + if err != nil { + t.Fatal(err) + } + + ref, err := name.ParseReference("ubuntu") + if err != nil { + t.Fatal(err) + } + + img = &mountableImage{ + Image: img, + Reference: ref, + } + + if err := validate.Image(img); err != nil { + t.Errorf("Validate() = %v", err) + } + + layers, err := img.Layers() + if err != nil { + t.Fatal(err) + } + + for i, l := range layers { + if _, ok := l.(*MountableLayer); !ok { + t.Errorf("layers[%d] should be MountableLayer but isn't", i) + } + } +} diff --git a/pkg/v1/remote/multi_write.go b/pkg/v1/remote/multi_write.go new file mode 100644 index 0000000..7f32413 --- /dev/null +++ b/pkg/v1/remote/multi_write.go @@ -0,0 +1,302 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" + "golang.org/x/sync/errgroup" +) + +// MultiWrite writes the given Images or ImageIndexes to the given refs, as +// efficiently as possible, by deduping shared layer blobs and uploading layers +// in parallel, then uploading all manifests in parallel. +// +// Current limitations: +// - All refs must share the same repository. +// - Images cannot consist of stream.Layers. +func MultiWrite(m map[name.Reference]Taggable, options ...Option) (rerr error) { + // Determine the repository being pushed to; if asked to push to + // multiple repositories, give up. + var repo, zero name.Repository + for ref := range m { + if repo == zero { + repo = ref.Context() + } else if ref.Context() != repo { + return fmt.Errorf("MultiWrite can only push to the same repository (saw %q and %q)", repo, ref.Context()) + } + } + + o, err := makeOptions(repo, options...) + if err != nil { + return err + } + + // Collect unique blobs (layers and config blobs). + blobs := map[v1.Hash]v1.Layer{} + newManifests := []map[name.Reference]Taggable{} + // Separate originally requested images and indexes, so we can push images first. + images, indexes := map[name.Reference]Taggable{}, map[name.Reference]Taggable{} + for ref, i := range m { + if img, ok := i.(v1.Image); ok { + images[ref] = i + if err := addImageBlobs(img, blobs, o.allowNondistributableArtifacts); err != nil { + return err + } + continue + } + if idx, ok := i.(v1.ImageIndex); ok { + indexes[ref] = i + newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, 0, o.allowNondistributableArtifacts) + if err != nil { + return err + } + continue + } + return fmt.Errorf("pushable resource was not Image or ImageIndex: %T", i) + } + + // Determine if any of the layers are Mountable, because if so we need + // to request Pull scope too. + ls := []v1.Layer{} + for _, l := range blobs { + ls = append(ls, l) + } + scopes := scopesForUploadingImage(repo, ls) + tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + w := writer{ + repo: repo, + client: &http.Client{Transport: tr}, + backoff: o.retryBackoff, + predicate: o.retryPredicate, + } + + // Collect the total size of blobs and manifests we're about to write. + if o.updates != nil { + w.progress = &progress{updates: o.updates} + w.progress.lastUpdate = &v1.Update{} + defer close(o.updates) + defer func() { _ = w.progress.err(rerr) }() + for _, b := range blobs { + size, err := b.Size() + if err != nil { + return err + } + w.progress.total(size) + } + countManifest := func(t Taggable) error { + b, err := t.RawManifest() + if err != nil { + return err + } + w.progress.total(int64(len(b))) + return nil + } + for _, i := range images { + if err := countManifest(i); err != nil { + return err + } + } + for _, nm := range newManifests { + for _, i := range nm { + if err := countManifest(i); err != nil { + return err + } + } + } + for _, i := range indexes { + if err := countManifest(i); err != nil { + return err + } + } + } + + // Upload individual blobs and collect any errors. + blobChan := make(chan v1.Layer, 2*o.jobs) + ctx := o.context + g, gctx := errgroup.WithContext(o.context) + for i := 0; i < o.jobs; i++ { + // Start N workers consuming blobs to upload. + g.Go(func() error { + for b := range blobChan { + if err := w.uploadOne(gctx, b); err != nil { + return err + } + } + return nil + }) + } + g.Go(func() error { + defer close(blobChan) + for _, b := range blobs { + select { + case blobChan <- b: + case <-gctx.Done(): + return gctx.Err() + } + } + return nil + }) + if err := g.Wait(); err != nil { + return err + } + + commitMany := func(ctx context.Context, m map[name.Reference]Taggable) error { + g, ctx := errgroup.WithContext(ctx) + // With all of the constituent elements uploaded, upload the manifests + // to commit the images and indexes, and collect any errors. + type task struct { + i Taggable + ref name.Reference + } + taskChan := make(chan task, 2*o.jobs) + for i := 0; i < o.jobs; i++ { + // Start N workers consuming tasks to upload manifests. + g.Go(func() error { + for t := range taskChan { + if err := w.commitManifest(ctx, t.i, t.ref); err != nil { + return err + } + } + return nil + }) + } + go func() { + for ref, i := range m { + taskChan <- task{i, ref} + } + close(taskChan) + }() + return g.Wait() + } + // Push originally requested image manifests. These have no + // dependencies. + if err := commitMany(ctx, images); err != nil { + return err + } + // Push new manifests from lowest levels up. + for i := len(newManifests) - 1; i >= 0; i-- { + if err := commitMany(ctx, newManifests[i]); err != nil { + return err + } + } + // Push originally requested index manifests, which might depend on + // newly discovered manifests. + + return commitMany(ctx, indexes) +} + +// addIndexBlobs adds blobs to the set of blobs we intend to upload, and +// returns the latest copy of the ordered collection of manifests to upload. +func addIndexBlobs(idx v1.ImageIndex, blobs map[v1.Hash]v1.Layer, repo name.Repository, newManifests []map[name.Reference]Taggable, lvl int, allowNondistributableArtifacts bool) ([]map[name.Reference]Taggable, error) { + if lvl > len(newManifests)-1 { + newManifests = append(newManifests, map[name.Reference]Taggable{}) + } + + im, err := idx.IndexManifest() + if err != nil { + return nil, err + } + for _, desc := range im.Manifests { + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + idx, err := idx.ImageIndex(desc.Digest) + if err != nil { + return nil, err + } + newManifests, err = addIndexBlobs(idx, blobs, repo, newManifests, lvl+1, allowNondistributableArtifacts) + if err != nil { + return nil, err + } + + // Also track the sub-index manifest to upload later by digest. + newManifests[lvl][repo.Digest(desc.Digest.String())] = idx + case types.OCIManifestSchema1, types.DockerManifestSchema2: + img, err := idx.Image(desc.Digest) + if err != nil { + return nil, err + } + if err := addImageBlobs(img, blobs, allowNondistributableArtifacts); err != nil { + return nil, err + } + + // Also track the sub-image manifest to upload later by digest. + newManifests[lvl][repo.Digest(desc.Digest.String())] = img + default: + // Workaround for #819. + if wl, ok := idx.(withLayer); ok { + layer, err := wl.Layer(desc.Digest) + if err != nil { + return nil, err + } + if err := addLayerBlob(layer, blobs, allowNondistributableArtifacts); err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("unknown media type: %v", desc.MediaType) + } + } + } + return newManifests, nil +} + +func addLayerBlob(l v1.Layer, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error { + // Ignore foreign layers. + mt, err := l.MediaType() + if err != nil { + return err + } + + if mt.IsDistributable() || allowNondistributableArtifacts { + d, err := l.Digest() + if err != nil { + return err + } + + blobs[d] = l + } + + return nil +} + +func addImageBlobs(img v1.Image, blobs map[v1.Hash]v1.Layer, allowNondistributableArtifacts bool) error { + ls, err := img.Layers() + if err != nil { + return err + } + // Collect all layers. + for _, l := range ls { + if err := addLayerBlob(l, blobs, allowNondistributableArtifacts); err != nil { + return err + } + } + + // Collect config blob. + cl, err := partial.ConfigLayer(img) + if err != nil { + return err + } + return addLayerBlob(cl, blobs, allowNondistributableArtifacts) +} diff --git a/pkg/v1/remote/multi_write_test.go b/pkg/v1/remote/multi_write_test.go new file mode 100644 index 0000000..c2dd2f0 --- /dev/null +++ b/pkg/v1/remote/multi_write_test.go @@ -0,0 +1,351 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "io" + "log" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestMultiWrite(t *testing.T) { + // Create a random image. + img1, err := random.Image(1024, 2) + if err != nil { + t.Fatal("random.Image:", err) + } + + // Create another image that's based on the first. + rl, err := random.Layer(1024, types.OCIUncompressedLayer) + if err != nil { + t.Fatal("random.Layer:", err) + } + img2, err := mutate.AppendLayers(img1, rl) + if err != nil { + t.Fatal("mutate.AppendLayers:", err) + } + + // Also create a random index of images. + subidx, err := random.Index(1024, 2, 3) + if err != nil { + t.Fatal("random.Index:", err) + } + + // Add a sub-sub-index of random images. + subsubidx, err := random.Index(1024, 3, 4) + if err != nil { + t.Fatal("random.Index:", err) + } + subidx = mutate.AppendManifests(subidx, mutate.IndexAddendum{Add: subsubidx}) + + // Create an index containing both images and the index above. + idx := mutate.AppendManifests(empty.Index, + mutate.IndexAddendum{Add: img1}, + mutate.IndexAddendum{Add: img2}, + mutate.IndexAddendum{Add: subidx}, + mutate.IndexAddendum{Add: rl}, + ) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + // Write both images and the manifest list. + tag1, tag2, tag3 := mustNewTag(t, u.Host+"/repo:tag1"), mustNewTag(t, u.Host+"/repo:tag2"), mustNewTag(t, u.Host+"/repo:tag3") + if err := MultiWrite(map[name.Reference]Taggable{ + tag1: img1, + tag2: img2, + tag3: idx, + }); err != nil { + t.Error("Write:", err) + } + + // Check that tagged images are present. + for _, tag := range []name.Tag{tag1, tag2} { + got, err := Image(tag) + if err != nil { + t.Error(err) + continue + } + if err := validate.Image(got); err != nil { + t.Error("Validate() =", err) + } + } + + // Check that tagged manfest list is present and valid. + got, err := Index(tag3) + if err != nil { + t.Fatal(err) + } + if err := validate.Index(got); err != nil { + t.Error("Validate() =", err) + } +} + +func TestMultiWriteWithNondistributableLayer(t *testing.T) { + // Create a random image. + img1, err := random.Image(1024, 2) + if err != nil { + t.Fatal("random.Image:", err) + } + + // Create another image that's based on the first. + rl, err := random.Layer(1024, types.OCIRestrictedLayer) + if err != nil { + t.Fatal("random.Layer:", err) + } + img, err := mutate.AppendLayers(img1, rl) + if err != nil { + t.Fatal("mutate.AppendLayers:", err) + } + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + // Write the image. + tag1 := mustNewTag(t, u.Host+"/repo:tag1") + if err := MultiWrite(map[name.Reference]Taggable{tag1: img}, WithNondistributable); err != nil { + t.Error("Write:", err) + } + + // Check that tagged image is present. + got, err := Image(tag1) + if err != nil { + t.Error(err) + } + if err := validate.Image(got); err != nil { + t.Error("Validate() =", err) + } +} + +func TestMultiWrite_Retry(t *testing.T) { + // Create a random image. + img1, err := random.Image(1024, 2) + if err != nil { + t.Fatal("random.Image:", err) + } + + t.Run("retry http error 500", func(t *testing.T) { + // Set up a fake registry. + handler := registry.New() + + numOfInternalServerErrors := 0 + registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if strings.Contains(request.URL.Path, "/manifests/") && numOfInternalServerErrors < 1 { + numOfInternalServerErrors++ + responseWriter.WriteHeader(500) + return + } + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatFailsOnFirstUpload) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tag1 := mustNewTag(t, u.Host+"/repo:tag1") + if err := MultiWrite(map[name.Reference]Taggable{ + tag1: img1, + }, WithRetryBackoff(fastBackoff)); err != nil { + t.Error("Write:", err) + } + }) + + t.Run("do not retry http error 401", func(t *testing.T) { + // Set up a fake registry. + handler := registry.New() + + numOf401HttpErrors := 0 + registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if strings.Contains(request.URL.Path, "/manifests/") { + numOf401HttpErrors++ + responseWriter.WriteHeader(401) + return + } + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatFailsOnFirstUpload) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tag1 := mustNewTag(t, u.Host+"/repo:tag1") + if err := MultiWrite(map[name.Reference]Taggable{ + tag1: img1, + }); err == nil { + t.Fatal("Expected error:") + } + + if numOf401HttpErrors > 1 { + t.Fatal("Should not retry on 401 errors:") + } + }) + + t.Run("do not retry transport errors if transport.Wrapper is used", func(t *testing.T) { + // reference a http server that is not listening (used to pick a port that isn't listening) + onlyHandlesPing := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if strings.HasSuffix(request.URL.Path, "/v2/") { + responseWriter.WriteHeader(200) + return + } + }) + s := httptest.NewServer(onlyHandlesPing) + defer s.Close() + + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tag1 := mustNewTag(t, u.Host+"/repo:tag1") + + // using a transport.Wrapper, meaning retry logic should not be wrapped + doesNotRetryTransport := &countTransport{inner: http.DefaultTransport} + transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, doesNotRetryTransport, nil) + if err != nil { + t.Fatal(err) + } + + noRetry := func(error) bool { return false } + + if err := MultiWrite(map[name.Reference]Taggable{ + tag1: img1, + }, WithTransport(transportWrapper), WithJobs(1), WithRetryPredicate(noRetry)); err == nil { + t.Errorf("Expected an error, got nil") + } + + // expect count == 1 since jobs is set to 1 and we should not retry on transport eof error + if doesNotRetryTransport.count != 1 { + t.Errorf("Incorrect count, got %d, want %d", doesNotRetryTransport.count, 1) + } + }) + + t.Run("do not add UserAgent if transport.Wrapper is used", func(t *testing.T) { + expectedNotUsedUserAgent := "TEST_USER_AGENT" + + handler := registry.New() + + registryThatAssertsUserAgentIsCorrect := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if strings.Contains(request.Header.Get("User-Agent"), expectedNotUsedUserAgent) { + t.Fatalf("Should not contain User-Agent: %s, Got: %s", expectedNotUsedUserAgent, request.Header.Get("User-Agent")) + } + + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatAssertsUserAgentIsCorrect) + + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + tag1 := mustNewTag(t, u.Host+"/repo:tag1") + // using a transport.Wrapper, meaning retry logic should not be wrapped + transportWrapper, err := transport.NewWithContext(context.Background(), tag1.Repository.Registry, authn.Anonymous, http.DefaultTransport, nil) + if err != nil { + t.Fatal(err) + } + + if err := MultiWrite(map[name.Reference]Taggable{ + tag1: img1, + }, WithTransport(transportWrapper), WithUserAgent(expectedNotUsedUserAgent)); err != nil { + t.Fatal(err) + } + }) +} + +// TestMultiWrite_Deep tests that a deeply nested tree of manifest lists gets +// pushed in the correct order (i.e., each level in sequence). +func TestMultiWrite_Deep(t *testing.T) { + idx, err := random.Index(1024, 2, 2) + if err != nil { + t.Fatal("random.Image:", err) + } + for i := 0; i < 4; i++ { + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{Add: idx}) + } + + // Set up a fake registry (with NOP logger to avoid spamming test logs). + nopLog := log.New(io.Discard, "", 0) + s := httptest.NewServer(registry.New(registry.Logger(nopLog))) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + // Write both images and the manifest list. + tag := mustNewTag(t, u.Host+"/repo:tag") + if err := MultiWrite(map[name.Reference]Taggable{ + tag: idx, + }); err != nil { + t.Error("Write:", err) + } + + // Check that tagged manfest list is present and valid. + got, err := Index(tag) + if err != nil { + t.Fatal(err) + } + if err := validate.Index(got); err != nil { + t.Error("Validate() =", err) + } +} + +type countTransport struct { + count int + inner http.RoundTripper +} + +func (t *countTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if strings.HasSuffix(req.URL.Path, "/v2/") { + return t.inner.RoundTrip(req) + } + + t.count++ + return nil, io.ErrUnexpectedEOF +} diff --git a/pkg/v1/remote/options.go b/pkg/v1/remote/options.go new file mode 100644 index 0000000..54a0af2 --- /dev/null +++ b/pkg/v1/remote/options.go @@ -0,0 +1,317 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "context" + "errors" + "io" + "net" + "net/http" + "syscall" + "time" + + "github.com/google/go-containerregistry/internal/retry" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +// Option is a functional option for remote operations. +type Option func(*options) error + +type options struct { + auth authn.Authenticator + keychain authn.Keychain + transport http.RoundTripper + platform v1.Platform + context context.Context + jobs int + userAgent string + allowNondistributableArtifacts bool + updates chan<- v1.Update + pageSize int + retryBackoff Backoff + retryPredicate retry.Predicate + filter map[string]string +} + +var defaultPlatform = v1.Platform{ + Architecture: "amd64", + OS: "linux", +} + +// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib +type Backoff = retry.Backoff + +var defaultRetryPredicate retry.Predicate = func(err error) bool { + // Various failure modes here, as we're often reading from and writing to + // the network. + if retry.IsTemporary(err) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) { + logs.Warn.Printf("retrying %v", err) + return true + } + return false +} + +// Try this three times, waiting 1s after first failure, 3s after second. +var defaultRetryBackoff = Backoff{ + Duration: 1.0 * time.Second, + Factor: 3.0, + Jitter: 0.1, + Steps: 3, +} + +// Useful for tests +var fastBackoff = Backoff{ + Duration: 1.0 * time.Millisecond, + Factor: 3.0, + Jitter: 0.1, + Steps: 3, +} + +var retryableStatusCodes = []int{ + http.StatusRequestTimeout, + http.StatusInternalServerError, + http.StatusBadGateway, + http.StatusServiceUnavailable, + http.StatusGatewayTimeout, +} + +const ( + defaultJobs = 4 + + // ECR returns an error if n > 1000: + // https://github.com/google/go-containerregistry/issues/1091 + defaultPageSize = 1000 +) + +// DefaultTransport is based on http.DefaultTransport with modifications +// documented inline below. +var DefaultTransport http.RoundTripper = &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, +} + +func makeOptions(target authn.Resource, opts ...Option) (*options, error) { + o := &options{ + transport: DefaultTransport, + platform: defaultPlatform, + context: context.Background(), + jobs: defaultJobs, + pageSize: defaultPageSize, + retryPredicate: defaultRetryPredicate, + retryBackoff: defaultRetryBackoff, + } + + for _, option := range opts { + if err := option(o); err != nil { + return nil, err + } + } + + switch { + case o.auth != nil && o.keychain != nil: + // It is a better experience to explicitly tell a caller their auth is misconfigured + // than potentially fail silently when the correct auth is overridden by option misuse. + return nil, errors.New("provide an option for either authn.Authenticator or authn.Keychain, not both") + case o.keychain != nil: + auth, err := o.keychain.Resolve(target) + if err != nil { + return nil, err + } + o.auth = auth + case o.auth == nil: + o.auth = authn.Anonymous + } + + // transport.Wrapper is a signal that consumers are opt-ing into providing their own transport without any additional wrapping. + // This is to allow consumers full control over the transports logic, such as providing retry logic. + if _, ok := o.transport.(*transport.Wrapper); !ok { + // Wrap the transport in something that logs requests and responses. + // It's expensive to generate the dumps, so skip it if we're writing + // to nothing. + if logs.Enabled(logs.Debug) { + o.transport = transport.NewLogger(o.transport) + } + + // Wrap the transport in something that can retry network flakes. + o.transport = transport.NewRetry(o.transport, transport.WithRetryPredicate(defaultRetryPredicate), transport.WithRetryStatusCodes(retryableStatusCodes...)) + + // Wrap this last to prevent transport.New from double-wrapping. + if o.userAgent != "" { + o.transport = transport.NewUserAgent(o.transport, o.userAgent) + } + } + + return o, nil +} + +// WithTransport is a functional option for overriding the default transport +// for remote operations. +// If transport.Wrapper is provided, this signals that the consumer does *not* want any further wrapping to occur. +// i.e. logging, retry and useragent +// +// The default transport is DefaultTransport. +func WithTransport(t http.RoundTripper) Option { + return func(o *options) error { + o.transport = t + return nil + } +} + +// WithAuth is a functional option for overriding the default authenticator +// for remote operations. +// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set. +// +// The default authenticator is authn.Anonymous. +func WithAuth(auth authn.Authenticator) Option { + return func(o *options) error { + o.auth = auth + return nil + } +} + +// WithAuthFromKeychain is a functional option for overriding the default +// authenticator for remote operations, using an authn.Keychain to find +// credentials. +// It is an error to use both WithAuth and WithAuthFromKeychain in the same Option set. +// +// The default authenticator is authn.Anonymous. +func WithAuthFromKeychain(keys authn.Keychain) Option { + return func(o *options) error { + o.keychain = keys + return nil + } +} + +// WithPlatform is a functional option for overriding the default platform +// that Image and Descriptor.Image use for resolving an index to an image. +// +// The default platform is amd64/linux. +func WithPlatform(p v1.Platform) Option { + return func(o *options) error { + o.platform = p + return nil + } +} + +// WithContext is a functional option for setting the context in http requests +// performed by a given function. Note that this context is used for _all_ +// http requests, not just the initial volley. E.g., for remote.Image, the +// context will be set on http requests generated by subsequent calls to +// RawConfigFile() and even methods on layers returned by Layers(). +// +// The default context is context.Background(). +func WithContext(ctx context.Context) Option { + return func(o *options) error { + o.context = ctx + return nil + } +} + +// WithJobs is a functional option for setting the parallelism of remote +// operations performed by a given function. Note that not all remote +// operations support parallelism. +// +// The default value is 4. +func WithJobs(jobs int) Option { + return func(o *options) error { + if jobs <= 0 { + return errors.New("jobs must be greater than zero") + } + o.jobs = jobs + return nil + } +} + +// WithUserAgent adds the given string to the User-Agent header for any HTTP +// requests. This header will also include "go-containerregistry/${version}". +// +// If you want to completely overwrite the User-Agent header, use WithTransport. +func WithUserAgent(ua string) Option { + return func(o *options) error { + o.userAgent = ua + return nil + } +} + +// WithNondistributable includes non-distributable (foreign) layers +// when writing images, see: +// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers +// +// The default behaviour is to skip these layers +func WithNondistributable(o *options) error { + o.allowNondistributableArtifacts = true + return nil +} + +// WithProgress takes a channel that will receive progress updates as bytes are written. +// +// Sending updates to an unbuffered channel will block writes, so callers +// should provide a buffered channel to avoid potential deadlocks. +func WithProgress(updates chan<- v1.Update) Option { + return func(o *options) error { + o.updates = updates + return nil + } +} + +// WithPageSize sets the given size as the value of parameter 'n' in the request. +// +// To omit the `n` parameter entirely, use WithPageSize(0). +// The default value is 1000. +func WithPageSize(size int) Option { + return func(o *options) error { + o.pageSize = size + return nil + } +} + +// WithRetryBackoff sets the httpBackoff for retry HTTP operations. +func WithRetryBackoff(backoff Backoff) Option { + return func(o *options) error { + o.retryBackoff = backoff + return nil + } +} + +// WithRetryPredicate sets the predicate for retry HTTP operations. +func WithRetryPredicate(predicate retry.Predicate) Option { + return func(o *options) error { + o.retryPredicate = predicate + return nil + } +} + +// WithFilter sets the filter querystring for HTTP operations. +func WithFilter(key string, value string) Option { + return func(o *options) error { + if o.filter == nil { + o.filter = map[string]string{} + } + o.filter[key] = value + return nil + } +} diff --git a/pkg/v1/remote/progress.go b/pkg/v1/remote/progress.go new file mode 100644 index 0000000..1f43963 --- /dev/null +++ b/pkg/v1/remote/progress.go @@ -0,0 +1,69 @@ +// Copyright 2022 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "io" + "sync" + "sync/atomic" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +type progress struct { + sync.Mutex + updates chan<- v1.Update + lastUpdate *v1.Update +} + +func (p *progress) total(delta int64) { + atomic.AddInt64(&p.lastUpdate.Total, delta) +} + +func (p *progress) complete(delta int64) { + p.Lock() + defer p.Unlock() + p.updates <- v1.Update{ + Total: p.lastUpdate.Total, + Complete: atomic.AddInt64(&p.lastUpdate.Complete, delta), + } +} + +func (p *progress) err(err error) error { + if err != nil && p.updates != nil { + p.updates <- v1.Update{Error: err} + } + return err +} + +type progressReader struct { + rc io.ReadCloser + + count *int64 // number of bytes this reader has read, to support resetting on retry. + progress *progress +} + +func (r *progressReader) Read(b []byte) (int, error) { + n, err := r.rc.Read(b) + if err != nil { + return n, err + } + atomic.AddInt64(r.count, int64(n)) + // TODO: warn/debug log if sending takes too long, or if sending is blocked while context is canceled. + r.progress.complete(int64(n)) + return n, nil +} + +func (r *progressReader) Close() error { return r.rc.Close() } diff --git a/pkg/v1/remote/progress_test.go b/pkg/v1/remote/progress_test.go new file mode 100644 index 0000000..759c8ca --- /dev/null +++ b/pkg/v1/remote/progress_test.go @@ -0,0 +1,463 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestWriteLayer_Progress(t *testing.T) { + l, err := random.Layer(1000, types.OCIUncompressedLayer) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := WriteLayer(ref.Context(), l, WithProgress(c)); err != nil { + t.Fatalf("WriteLayer: %v", err) + } + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +// TestWriteLayer_Progress_Exists tests progress reporting behavior when the +// layer already exists in the registry, so writes are skipped, but progress +// should still be reported in one update. +func TestWriteLayer_Progress_Exists(t *testing.T) { + l, err := random.Layer(1000, types.OCILayer) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + // Write the layer, so we can get updates when we write it again. + if err := WriteLayer(ref.Context(), l); err != nil { + t.Fatalf("WriteLayer: %v", err) + } + if err := WriteLayer(ref.Context(), l, WithProgress(c)); err != nil { + t.Fatalf("WriteLayer: %v", err) + } + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +func TestWrite_Progress(t *testing.T) { + img, err := random.Image(1000, 5) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, img, WithProgress(c)); err != nil { + t.Fatalf("Write: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +// An image with multiple identical layers is handled correctly. +func TestWrite_Progress_DedupeLayers(t *testing.T) { + img := empty.Image + for i := 0; i < 10; i++ { + l, err := random.Layer(1000, types.OCILayer) + if err != nil { + t.Fatal(err) + } + + img, err = mutate.AppendLayers(img, l) + if err != nil { + t.Fatal(err) + } + } + + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, img, WithProgress(c)); err != nil { + t.Fatalf("Write: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +func TestWriteIndex_Progress(t *testing.T) { + idx, err := random.Index(1000, 3, 3) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := WriteIndex(ref, idx, WithProgress(c)); err != nil { + t.Fatalf("WriteIndex: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +func TestMultiWrite_Progress(t *testing.T) { + idx, err := random.Index(1000, 3, 3) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 1000) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + ref, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload", u.Host)) + if err != nil { + t.Fatal(err) + } + ref2, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload:again", u.Host)) + if err != nil { + t.Fatal(err) + } + + if err := MultiWrite(map[name.Reference]Taggable{ + ref: idx, + ref2: idx, + }, WithProgress(c)); err != nil { + t.Fatalf("MultiWrite: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +func TestMultiWrite_Progress_Retry(t *testing.T) { + idx, err := random.Index(1000, 3, 3) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 1000) + + // Set up a fake registry. + handler := registry.New() + numOfInternalServerErrors := 0 + var mu sync.Mutex + registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + mu.Lock() + defer mu.Unlock() + if strings.Contains(request.URL.Path, "/manifests/") && numOfInternalServerErrors < 1 { + numOfInternalServerErrors++ + responseWriter.WriteHeader(500) + return + } + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatFailsOnFirstUpload) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + ref, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload", u.Host)) + if err != nil { + t.Fatal(err) + } + ref2, err := name.ParseReference(fmt.Sprintf("%s/test/progress/upload:again", u.Host)) + if err != nil { + t.Fatal(err) + } + + if err := MultiWrite(map[name.Reference]Taggable{ + ref: idx, + ref2: idx, + }, WithProgress(c), WithRetryBackoff(fastBackoff)); err != nil { + t.Fatalf("MultiWrite: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +func TestWriteLayer_Progress_Retry(t *testing.T) { + l, err := random.Layer(100000, types.OCIUncompressedLayer) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + handler := registry.New() + + numOfInternalServerErrors := 0 + registryThatFailsOnFirstUpload := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if request.Method == http.MethodPatch && strings.Contains(request.URL.Path, "upload/blobs/uploads") && numOfInternalServerErrors < 1 { + numOfInternalServerErrors++ + responseWriter.WriteHeader(500) + return + } + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatFailsOnFirstUpload) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := WriteLayer(ref.Context(), l, WithProgress(c), WithRetryBackoff(fastBackoff)); err != nil { + t.Fatalf("WriteLayer: %v", err) + } + + everyUpdate := []v1.Update{} + for update := range c { + everyUpdate = append(everyUpdate, update) + } + + if diff := cmp.Diff(everyUpdate, []v1.Update{ + {Total: 101921, Complete: 32768}, + {Total: 101921, Complete: 65536}, + {Total: 101921, Complete: 98304}, + {Total: 101921, Complete: 101921}, + // retry results in the same messages sent to the updates channel + {Total: 101921, Complete: 0}, + {Total: 101921, Complete: 32768}, + {Total: 101921, Complete: 65536}, + {Total: 101921, Complete: 98304}, + {Total: 101921, Complete: 101921}, + }); diff != "" { + t.Errorf("received updates (-want +got) = %s", diff) + } +} + +func TestWriteLayer_Progress_Error(t *testing.T) { + l, err := random.Layer(100000, types.OCIUncompressedLayer) + if err != nil { + t.Fatal(err) + } + c := make(chan v1.Update, 200) + + // Set up a fake registry. + handler := registry.New() + registryThatAlwaysFails := http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) { + if request.Method == http.MethodPatch && strings.Contains(request.URL.Path, "blobs/uploads") { + responseWriter.WriteHeader(403) + } + handler.ServeHTTP(responseWriter, request) + }) + + s := httptest.NewServer(registryThatAlwaysFails) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := WriteLayer(ref.Context(), l, WithProgress(c)); err == nil { + t.Errorf("WriteLayer: wanted error, got nil") + } + + everyUpdate := []v1.Update{} + for update := range c { + everyUpdate = append(everyUpdate, update) + } + + if diff := cmp.Diff(everyUpdate[:len(everyUpdate)-1], []v1.Update{ + {Total: 101921, Complete: 32768}, + {Total: 101921, Complete: 65536}, + {Total: 101921, Complete: 98304}, + {Total: 101921, Complete: 101921}, + // retry results in the same messages sent to the updates channel + {Total: 101921, Complete: 0}, + }); diff != "" { + t.Errorf("received updates (-want +got) = %s", diff) + } + if everyUpdate[len(everyUpdate)-1].Error == nil { + t.Errorf("Last update had nil error") + } +} + +func TestWrite_Progress_WithNonDistributableLayer_AndIncludeNonDistributableLayersOption(t *testing.T) { + ociLayer, err := random.Layer(1000, types.OCILayer) + if err != nil { + t.Fatal(err) + } + + nonDistributableLayer, err := random.Layer(1000, types.OCIRestrictedLayer) + if err != nil { + t.Fatal(err) + } + + img, err := mutate.AppendLayers(empty.Image, ociLayer, nonDistributableLayer) + if err != nil { + t.Fatal(err) + } + + c := make(chan v1.Update, 200) + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/progress/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, img, WithProgress(c), WithNondistributable); err != nil { + t.Fatalf("Write: %v", err) + } + + if err := checkUpdates(c); err != nil { + t.Fatal(err) + } +} + +// checkUpdates checks that updates show steady progress toward a total, and +// don't describe errors. +func checkUpdates(updates <-chan v1.Update) error { + var high, total int64 + for u := range updates { + if u.Error != nil { + return u.Error + } + + if u.Total == 0 { + return errors.New("saw zero total") + } + + if total == 0 { + total = u.Total + } else if u.Total != total { + return fmt.Errorf("total changed: was %d, saw %d", total, u.Total) + } + + if u.Complete < high { + return fmt.Errorf("saw progress revert: was high of %d, saw %d", high, u.Complete) + } + high = u.Complete + } + + if high > total { + return fmt.Errorf("final progress (%d) exceeded total (%d) by %d", high, total, high-total) + } else if high < total { + return fmt.Errorf("final progress (%d) did not reach total (%d) by %d", high, total, total-high) + } + + return nil +} diff --git a/pkg/v1/remote/referrers.go b/pkg/v1/remote/referrers.go new file mode 100644 index 0000000..b3db863 --- /dev/null +++ b/pkg/v1/remote/referrers.go @@ -0,0 +1,35 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// Referrers returns a list of descriptors that refer to the given manifest digest. +// +// The subject manifest doesn't have to exist in the registry for there to be descriptors that refer to it. +func Referrers(d name.Digest, options ...Option) (*v1.IndexManifest, error) { + o, err := makeOptions(d.Context(), options...) + if err != nil { + return nil, err + } + f, err := makeFetcher(d, o) + if err != nil { + return nil, err + } + return f.fetchReferrers(o.context, o.filter, d) +} diff --git a/pkg/v1/remote/referrers_test.go b/pkg/v1/remote/referrers_test.go new file mode 100644 index 0000000..91f9edc --- /dev/null +++ b/pkg/v1/remote/referrers_test.go @@ -0,0 +1,183 @@ +// Copyright 2023 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote_test + +import ( + "fmt" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestReferrers(t *testing.T) { + // Run all tests against: + // + // (1) A OCI 1.0 registry (without referrers API) + // (2) An OCI 1.1+ registry (with referrers API) + // + for _, leg := range []struct { + server *httptest.Server + tryFallback bool + }{ + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(false))), + tryFallback: true, + }, + { + server: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))), + tryFallback: false, + }, + } { + s := leg.server + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + + descriptor := func(img v1.Image) v1.Descriptor { + d, err := img.Digest() + if err != nil { + t.Fatal(err) + } + sz, err := img.Size() + if err != nil { + t.Fatal(err) + } + mt, err := img.MediaType() + if err != nil { + t.Fatal(err) + } + return v1.Descriptor{ + Digest: d, + Size: sz, + MediaType: mt, + ArtifactType: "application/testing123", + } + } + + // Push an image we'll attach things to. + // We'll copy from src to dst. + rootRef, err := name.ParseReference(fmt.Sprintf("%s/repo:root", u.Host)) + if err != nil { + t.Fatal(err) + } + rootImg, err := random.Image(10, 10) + if err != nil { + t.Fatal(err) + } + rootImg = mutate.ConfigMediaType(rootImg, types.MediaType("application/testing123")) + if err := remote.Write(rootRef, rootImg); err != nil { + t.Fatal(err) + } + rootDesc := descriptor(rootImg) + t.Logf("root image is %s", rootDesc.Digest) + + // Push an image that refers to the root image as its subject. + leafRef, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf", u.Host)) + if err != nil { + t.Fatal(err) + } + leafImg, err := random.Image(20, 20) + if err != nil { + t.Fatal(err) + } + leafImg = mutate.ConfigMediaType(leafImg, types.MediaType("application/testing123")) + leafImg = mutate.Subject(leafImg, rootDesc).(v1.Image) + if err := remote.Write(leafRef, leafImg); err != nil { + t.Fatal(err) + } + leafDesc := descriptor(leafImg) + t.Logf("leaf image is %s", leafDesc.Digest) + + // Get the referrers of the root image, by digest. + rootRefDigest := rootRef.Context().Digest(rootDesc.Digest.String()) + index, err := remote.Referrers(rootRefDigest) + if err != nil { + t.Fatal(err) + } + if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" { + t.Fatalf("referrers diff (-want,+got): %s", d) + } + + if leg.tryFallback { + // Get the referrers by querying the root image's fallback tag directly. + tag, err := name.ParseReference(fmt.Sprintf("%s/repo:sha256-%s", u.Host, rootDesc.Digest.Hex)) + if err != nil { + t.Fatal(err) + } + idx, err := remote.Index(tag) + if err != nil { + t.Fatal(err) + } + mf, err := idx.IndexManifest() + if err != nil { + t.Fatal(err) + } + if d := cmp.Diff(index.Manifests, mf.Manifests); d != "" { + t.Fatalf("fallback tag diff (-want,+got): %s", d) + } + } + + // Push the leaf image again, this time with a different tag. + // This shouldn't add another item to the root image's referrers, + // because it's the same digest. + // Push an image that refers to the root image as its subject. + leaf2Ref, err := name.ParseReference(fmt.Sprintf("%s/repo:leaf2", u.Host)) + if err != nil { + t.Fatal(err) + } + if err := remote.Write(leaf2Ref, leafImg); err != nil { + t.Fatal(err) + } + // Get the referrers of the root image again, which should only have one entry. + rootRefDigest = rootRef.Context().Digest(rootDesc.Digest.String()) + index, err = remote.Referrers(rootRefDigest) + if err != nil { + t.Fatal(err) + } + if d := cmp.Diff([]v1.Descriptor{leafDesc}, index.Manifests); d != "" { + t.Fatalf("referrers diff after second push (-want,+got): %s", d) + } + + // Try applying filters and verify number of manifests and and annotations + index, err = remote.Referrers(rootRefDigest, + remote.WithFilter("artifactType", "application/testing123")) + if err != nil { + t.Fatal(err) + } + if numManifests := len(index.Manifests); numManifests == 0 { + t.Fatal("index contained 0 manifests") + } + + index, err = remote.Referrers(rootRefDigest, + remote.WithFilter("artifactType", "application/testing123BADDDD")) + if err != nil { + t.Fatal(err) + } + if numManifests := len(index.Manifests); numManifests != 0 { + t.Fatalf("expected index to contain 0 manifests, but had %d", numManifests) + } + } +} diff --git a/pkg/v1/remote/transport/README.md b/pkg/v1/remote/transport/README.md new file mode 100644 index 0000000..bd4d957 --- /dev/null +++ b/pkg/v1/remote/transport/README.md @@ -0,0 +1,129 @@ +# `transport` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/transport?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/transport) + +The [distribution protocol](https://github.com/opencontainers/distribution-spec) is fairly simple, but correctly [implementing authentication](../../../authn/README.md) is **hard**. + +This package [implements](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#New) an [`http.RoundTripper`](https://godoc.org/net/http#RoundTripper) +that transparently performs: +* [Token +Authentication](https://docs.docker.com/registry/spec/auth/token/) and +* [OAuth2 +Authentication](https://docs.docker.com/registry/spec/auth/oauth/) + +for registry clients. + +## Raison d'être + +> Why not just use the [`docker/distribution`](https://godoc.org/github.com/docker/distribution/registry/client/auth) client? + +Great question! Mostly, because I don't want to depend on [`prometheus/client_golang`](https://github.com/prometheus/client_golang). + +As a performance optimization, that client uses [a cache](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/client/repository.go#L173) to keep track of a mapping between blob digests and their [descriptors](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/blobs.go#L57-L86). Unfortunately, the cache [uses prometheus](https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/storage/cache/cachedblobdescriptorstore.go#L44) to track hits and misses, so if you want to use that client you have to pull in all of prometheus, which is pretty large. + +![docker/distribution](../../../../images/docker.dot.svg) + +> Why does it matter if you depend on prometheus? Who cares? + +It's generally polite to your downstream to reduce the number of dependencies your package requires: + +* Downloading your package is faster, which helps our Australian friends and people on airplanes. +* There is less code to compile, which speeds up builds and saves the planet from global warming. +* You reduce the likelihood of inflicting dependency hell upon your consumers. +* [Tim Hockin](https://twitter.com/thockin/status/958606077456654336) prefers it based on his experience working on Kubernetes, and he's a pretty smart guy. + +> Okay, what about [`containerd/containerd`](https://godoc.org/github.com/containerd/containerd/remotes/docker)? + +Similar reasons! That ends up pulling in grpc, protobuf, and logrus. + +![containerd/containerd](../../../../images/containerd.dot.svg) + +> Well... what about [`containers/image`](https://godoc.org/github.com/containers/image/docker)? + +That just uses the the `docker/distribution` client... and more! + +![containers/image](../../../../images/containers.dot.svg) + +> Wow, what about this package? + +Of course, this package isn't perfect either. `transport` depends on `authn`, +which in turn depends on docker's config file parsing and handling package, +which you don't strictly need but almost certainly want if you're going to be +interacting with a registry. + +![google/go-containerregistry](../../../../images/ggcr.dot.svg) + +*These graphs were generated by +[`kisielk/godepgraph`](https://github.com/kisielk/godepgraph).* + +## Usage + +This is heavily used by the +[`remote`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote) +package, which implements higher level image-centric functionality, but this +package is useful if you want to interact directly with the registry to do +something that `remote` doesn't support, e.g. [to handle with schema 1 +images](https://github.com/google/go-containerregistry/pull/509). + +This package also includes some [error +handling](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#errors) +facilities in the form of +[`CheckError`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/remote/transport#CheckError), +which will parse the response body into a structured error for unexpected http +status codes. + +Here's a "simple" program that writes the result of +[listing tags](https://github.com/opencontainers/distribution-spec/blob/60be706c34ee7805bdd1d3d11affec53b0dfb8fb/spec.md#tags) +for [`gcr.io/google-containers/pause`](https://gcr.io/google-containers/pause) +to stdout. + +```go +package main + +import ( + "io" + "net/http" + "os" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" +) + +func main() { + repo, err := name.NewRepository("gcr.io/google-containers/pause") + if err != nil { + panic(err) + } + + // Fetch credentials based on your docker config file, which is $HOME/.docker/config.json or $DOCKER_CONFIG. + auth, err := authn.DefaultKeychain.Resolve(repo.Registry) + if err != nil { + panic(err) + } + + // Construct an http.Client that is authorized to pull from gcr.io/google-containers/pause. + scopes := []string{repo.Scope(transport.PullScope)} + t, err := transport.New(repo.Registry, auth, http.DefaultTransport, scopes) + if err != nil { + panic(err) + } + client := &http.Client{Transport: t} + + // Make the actual request. + resp, err := client.Get("https://gcr.io/v2/google-containers/pause/tags/list") + if err != nil { + panic(err) + } + + // Assert that we get a 200, otherwise attempt to parse body as a structured error. + if err := transport.CheckError(resp, http.StatusOK); err != nil { + panic(err) + } + + // Write the response to stdout. + if _, err := io.Copy(os.Stdout, resp.Body); err != nil { + panic(err) + } +} +``` diff --git a/pkg/v1/remote/transport/basic.go b/pkg/v1/remote/transport/basic.go new file mode 100644 index 0000000..fdb362b --- /dev/null +++ b/pkg/v1/remote/transport/basic.go @@ -0,0 +1,62 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "encoding/base64" + "fmt" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" +) + +type basicTransport struct { + inner http.RoundTripper + auth authn.Authenticator + target string +} + +var _ http.RoundTripper = (*basicTransport)(nil) + +// RoundTrip implements http.RoundTripper +func (bt *basicTransport) RoundTrip(in *http.Request) (*http.Response, error) { + if bt.auth != authn.Anonymous { + auth, err := bt.auth.Authorization() + if err != nil { + return nil, err + } + + // http.Client handles redirects at a layer above the http.RoundTripper + // abstraction, so to avoid forwarding Authorization headers to places + // we are redirected, only set it when the authorization header matches + // the host with which we are interacting. + // In case of redirect http.Client can use an empty Host, check URL too. + if in.Host == bt.target || in.URL.Host == bt.target { + if bearer := auth.RegistryToken; bearer != "" { + hdr := fmt.Sprintf("Bearer %s", bearer) + in.Header.Set("Authorization", hdr) + } else if user, pass := auth.Username, auth.Password; user != "" && pass != "" { + delimited := fmt.Sprintf("%s:%s", user, pass) + encoded := base64.StdEncoding.EncodeToString([]byte(delimited)) + hdr := fmt.Sprintf("Basic %s", encoded) + in.Header.Set("Authorization", hdr) + } else if token := auth.Auth; token != "" { + hdr := fmt.Sprintf("Basic %s", token) + in.Header.Set("Authorization", hdr) + } + } + } + return bt.inner.RoundTrip(in) +} diff --git a/pkg/v1/remote/transport/basic_test.go b/pkg/v1/remote/transport/basic_test.go new file mode 100644 index 0000000..68dd90e --- /dev/null +++ b/pkg/v1/remote/transport/basic_test.go @@ -0,0 +1,138 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" +) + +func TestBasicTransport(t *testing.T) { + username := "foo" + password := "bar" + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get("Authorization") + if !strings.HasPrefix(hdr, "Basic ") { + t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr) + } + user, pass, _ := r.BasicAuth() + if user != username || pass != password { + t.Error("Invalid credentials.") + } + if r.URL.Path == "/v2/auth" { + http.Redirect(w, r, "/redirect", http.StatusMovedPermanently) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + inner := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: username, Password: password} + client := http.Client{Transport: &basicTransport{inner: inner, auth: basic, target: "gcr.io"}} + + _, err := client.Get("http://gcr.io/v2/auth") + if err != nil { + t.Errorf("Unexpected error during Get: %v", err) + } +} + +func TestBasicTransportRegistryToken(t *testing.T) { + token := "mytoken" + for _, tc := range []struct { + auth authn.Authenticator + hdr string + wantErr bool + }{{ + auth: authn.FromConfig(authn.AuthConfig{RegistryToken: token}), + hdr: "Bearer mytoken", + }, { + auth: authn.FromConfig(authn.AuthConfig{Auth: token}), + hdr: "Basic mytoken", + }, { + auth: authn.Anonymous, + hdr: "", + }, { + auth: &badAuth{}, + hdr: "", + wantErr: true, + }} { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get("Authorization") + want := tc.hdr + if hdr != want { + t.Errorf("Header.Get(Authorization); got %v, want %s", hdr, want) + } + if r.URL.Path == "/v2/auth" { + http.Redirect(w, r, "/redirect", http.StatusMovedPermanently) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + inner := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + client := http.Client{Transport: &basicTransport{inner: inner, auth: tc.auth, target: "gcr.io"}} + + _, err := client.Get("http://gcr.io/v2/auth") + if err != nil && !tc.wantErr { + t.Errorf("Unexpected error during Get: %v", err) + } + } +} + +func TestBasicTransportWithEmptyAuthnCred(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if c, ok := r.Header["Authorization"]; ok && c[0] == "" { + t.Error("got empty Authorization header") + } + if r.URL.Path == "/v2/auth" { + http.Redirect(w, r, "/redirect", http.StatusMovedPermanently) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + inner := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + client := http.Client{Transport: &basicTransport{inner: inner, auth: authn.Anonymous, target: "gcr.io"}} + _, err := client.Get("http://gcr.io/v2/auth") + if err != nil { + t.Errorf("Unexpected error during Get: %v", err) + } +} diff --git a/pkg/v1/remote/transport/bearer.go b/pkg/v1/remote/transport/bearer.go new file mode 100644 index 0000000..ea07ff6 --- /dev/null +++ b/pkg/v1/remote/transport/bearer.go @@ -0,0 +1,320 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strings" + + authchallenge "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" +) + +type bearerTransport struct { + // Wrapped by bearerTransport. + inner http.RoundTripper + // Basic credentials that we exchange for bearer tokens. + basic authn.Authenticator + // Holds the bearer response from the token service. + bearer authn.AuthConfig + // Registry to which we send bearer tokens. + registry name.Registry + // See https://tools.ietf.org/html/rfc6750#section-3 + realm string + // See https://docs.docker.com/registry/spec/auth/token/ + service string + scopes []string + // Scheme we should use, determined by ping response. + scheme string +} + +var _ http.RoundTripper = (*bearerTransport)(nil) + +var portMap = map[string]string{ + "http": "80", + "https": "443", +} + +func stringSet(ss []string) map[string]struct{} { + set := make(map[string]struct{}) + for _, s := range ss { + set[s] = struct{}{} + } + return set +} + +// RoundTrip implements http.RoundTripper +func (bt *bearerTransport) RoundTrip(in *http.Request) (*http.Response, error) { + sendRequest := func() (*http.Response, error) { + // http.Client handles redirects at a layer above the http.RoundTripper + // abstraction, so to avoid forwarding Authorization headers to places + // we are redirected, only set it when the authorization header matches + // the registry with which we are interacting. + // In case of redirect http.Client can use an empty Host, check URL too. + if matchesHost(bt.registry, in, bt.scheme) { + hdr := fmt.Sprintf("Bearer %s", bt.bearer.RegistryToken) + in.Header.Set("Authorization", hdr) + } + return bt.inner.RoundTrip(in) + } + + res, err := sendRequest() + if err != nil { + return nil, err + } + + // If we hit a WWW-Authenticate challenge, it might be due to expired tokens or insufficient scope. + if challenges := authchallenge.ResponseChallenges(res); len(challenges) != 0 { + // close out old response, since we will not return it. + res.Body.Close() + + newScopes := []string{} + for _, wac := range challenges { + // TODO(jonjohnsonjr): Should we also update "realm" or "service"? + if want, ok := wac.Parameters["scope"]; ok { + // Add any scopes that we don't already request. + got := stringSet(bt.scopes) + if _, ok := got[want]; !ok { + newScopes = append(newScopes, want) + } + } + } + + // Some registries seem to only look at the first scope parameter during a token exchange. + // If a request fails because it's missing a scope, we should put those at the beginning, + // otherwise the registry might just ignore it :/ + newScopes = append(newScopes, bt.scopes...) + bt.scopes = newScopes + + // TODO(jonjohnsonjr): Teach transport.Error about "error" and "error_description" from challenge. + + // Retry the request to attempt to get a valid token. + if err = bt.refresh(in.Context()); err != nil { + return nil, err + } + return sendRequest() + } + + return res, err +} + +// It's unclear which authentication flow to use based purely on the protocol, +// so we rely on heuristics and fallbacks to support as many registries as possible. +// The basic token exchange is attempted first, falling back to the oauth flow. +// If the IdentityToken is set, this indicates that we should start with the oauth flow. +func (bt *bearerTransport) refresh(ctx context.Context) error { + auth, err := bt.basic.Authorization() + if err != nil { + return err + } + + if auth.RegistryToken != "" { + bt.bearer.RegistryToken = auth.RegistryToken + return nil + } + + var content []byte + if auth.IdentityToken != "" { + // If the secret being stored is an identity token, + // the Username should be set to <token>, which indicates + // we are using an oauth flow. + content, err = bt.refreshOauth(ctx) + var terr *Error + if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound { + // Note: Not all token servers implement oauth2. + // If the request to the endpoint returns 404 using the HTTP POST method, + // refer to Token Documentation for using the HTTP GET method supported by all token servers. + content, err = bt.refreshBasic(ctx) + } + } else { + content, err = bt.refreshBasic(ctx) + } + if err != nil { + return err + } + + // Some registries don't have "token" in the response. See #54. + type tokenResponse struct { + Token string `json:"token"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + // TODO: handle expiry? + } + + var response tokenResponse + if err := json.Unmarshal(content, &response); err != nil { + return err + } + + // Some registries set access_token instead of token. + if response.AccessToken != "" { + response.Token = response.AccessToken + } + + // Find a token to turn into a Bearer authenticator + if response.Token != "" { + bt.bearer.RegistryToken = response.Token + } else { + return fmt.Errorf("no token in bearer response:\n%s", content) + } + + // If we obtained a refresh token from the oauth flow, use that for refresh() now. + if response.RefreshToken != "" { + bt.basic = authn.FromConfig(authn.AuthConfig{ + IdentityToken: response.RefreshToken, + }) + } + + return nil +} + +func matchesHost(reg name.Registry, in *http.Request, scheme string) bool { + canonicalHeaderHost := canonicalAddress(in.Host, scheme) + canonicalURLHost := canonicalAddress(in.URL.Host, scheme) + canonicalRegistryHost := canonicalAddress(reg.RegistryStr(), scheme) + return canonicalHeaderHost == canonicalRegistryHost || canonicalURLHost == canonicalRegistryHost +} + +func canonicalAddress(host, scheme string) (address string) { + // The host may be any one of: + // - hostname + // - hostname:port + // - ipv4 + // - ipv4:port + // - ipv6 + // - [ipv6]:port + // As net.SplitHostPort returns an error if the host does not contain a port, we should only attempt + // to call it when we know that the address contains a port + if strings.Count(host, ":") == 1 || (strings.Count(host, ":") >= 2 && strings.Contains(host, "]:")) { + hostname, port, err := net.SplitHostPort(host) + if err != nil { + return host + } + if port == "" { + port = portMap[scheme] + } + + return net.JoinHostPort(hostname, port) + } + + return net.JoinHostPort(host, portMap[scheme]) +} + +// https://docs.docker.com/registry/spec/auth/oauth/ +func (bt *bearerTransport) refreshOauth(ctx context.Context) ([]byte, error) { + auth, err := bt.basic.Authorization() + if err != nil { + return nil, err + } + + u, err := url.Parse(bt.realm) + if err != nil { + return nil, err + } + + v := url.Values{} + v.Set("scope", strings.Join(bt.scopes, " ")) + if bt.service != "" { + v.Set("service", bt.service) + } + v.Set("client_id", defaultUserAgent) + if auth.IdentityToken != "" { + v.Set("grant_type", "refresh_token") + v.Set("refresh_token", auth.IdentityToken) + } else if auth.Username != "" && auth.Password != "" { + // TODO(#629): This is unreachable. + v.Set("grant_type", "password") + v.Set("username", auth.Username) + v.Set("password", auth.Password) + v.Set("access_type", "offline") + } + + client := http.Client{Transport: bt.inner} + req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(v.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + // We don't want to log credentials. + ctx = redact.NewContext(ctx, "oauth token response contains credentials") + + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := CheckError(resp, http.StatusOK); err != nil { + if bt.basic == authn.Anonymous { + logs.Warn.Printf("No matching credentials were found for %q", bt.registry) + } + return nil, err + } + + return io.ReadAll(resp.Body) +} + +// https://docs.docker.com/registry/spec/auth/token/ +func (bt *bearerTransport) refreshBasic(ctx context.Context) ([]byte, error) { + u, err := url.Parse(bt.realm) + if err != nil { + return nil, err + } + b := &basicTransport{ + inner: bt.inner, + auth: bt.basic, + target: u.Host, + } + client := http.Client{Transport: b} + + v := u.Query() + v["scope"] = bt.scopes + v.Set("service", bt.service) + u.RawQuery = v.Encode() + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + + // We don't want to log credentials. + ctx = redact.NewContext(ctx, "basic token response contains credentials") + + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err := CheckError(resp, http.StatusOK); err != nil { + if bt.basic == authn.Anonymous { + logs.Warn.Printf("No matching credentials were found for %q", bt.registry) + } + return nil, err + } + + return io.ReadAll(resp.Body) +} diff --git a/pkg/v1/remote/transport/bearer_test.go b/pkg/v1/remote/transport/bearer_test.go new file mode 100644 index 0000000..a03b1f9 --- /dev/null +++ b/pkg/v1/remote/transport/bearer_test.go @@ -0,0 +1,561 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" +) + +func TestBearerRefresh(t *testing.T) { + expectedToken := "Sup3rDup3rS3cr3tz" + expectedScope := "this-is-your-scope" + expectedService := "my-service.io" + + cases := []struct { + tokenKey string + wantErr bool + }{{ + tokenKey: "token", + wantErr: false, + }, { + tokenKey: "access_token", + wantErr: false, + }, { + tokenKey: "tolkien", + wantErr: true, + }} + + for _, tc := range cases { + t.Run(tc.tokenKey, func(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get("Authorization") + if !strings.HasPrefix(hdr, "Basic ") { + t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr) + } + if got, want := r.FormValue("scope"), expectedScope; got != want { + t.Errorf("FormValue(scope); got %v, want %v", got, want) + } + if got, want := r.FormValue("service"), expectedService; got != want { + t.Errorf("FormValue(service); got %v, want %v", got, want) + } + w.Write([]byte(fmt.Sprintf(`{%q: %q}`, tc.tokenKey, expectedToken))) + })) + defer server.Close() + + basic := &authn.Basic{Username: "foo", Password: "bar"} + registry, err := name.NewRegistry(expectedService, name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + bt := &bearerTransport{ + inner: http.DefaultTransport, + basic: basic, + registry: registry, + realm: server.URL, + scopes: []string{expectedScope}, + service: expectedService, + scheme: "http", + } + + if err := bt.refresh(context.Background()); (err != nil) != tc.wantErr { + t.Errorf("refresh() = %v", err) + } + }) + } +} + +func TestBearerTransport(t *testing.T) { + expectedToken := "sdkjhfskjdhfkjshdf" + + blobServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // We don't expect the blobServer to receive bearer tokens. + if got := r.Header.Get("Authorization"); got != "" { + t.Errorf("Header.Get(Authorization); got %v, want empty string", got) + } + w.WriteHeader(http.StatusOK) + })) + defer blobServer.Close() + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got, want := r.Header.Get("Authorization"), "Bearer "+expectedToken; got != want { + t.Errorf("Header.Get(Authorization); got %v, want %v", got, want) + } + if r.URL.Path == "/v2/auth" { + http.Redirect(w, r, "/redirect", http.StatusMovedPermanently) + return + } + if strings.Contains(r.URL.Path, "blobs") { + http.Redirect(w, r, blobServer.URL, http.StatusFound) + return + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Errorf("Unexpected error during url.Parse: %v", err) + } + registry, err := name.NewRegistry(u.Host, name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + client := http.Client{Transport: &bearerTransport{ + inner: &http.Transport{}, + bearer: authn.AuthConfig{RegistryToken: expectedToken}, + registry: registry, + scheme: "http", + }} + + _, err = client.Get(fmt.Sprintf("http://%s/v2/auth", u.Host)) + if err != nil { + t.Errorf("Unexpected error during Get: %v", err) + } + + _, err = client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Errorf("Unexpected error during Get: %v", err) + } +} + +func TestBearerTransportTokenRefresh(t *testing.T) { + initialToken := "foo" + refreshedToken := "bar" + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hdr := r.Header.Get("Authorization") + if hdr == "Bearer "+refreshedToken { + w.WriteHeader(http.StatusOK) + return + } + if strings.HasPrefix(hdr, "Basic ") { + w.Write([]byte(fmt.Sprintf(`{"token": %q}`, refreshedToken))) + } + + w.Header().Set("WWW-Authenticate", "scope=foo") + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + registry, err := name.NewRegistry(u.Host, name.WeakValidation) + if err != nil { + t.Fatalf("Unexpected error during NewRegistry: %v", err) + } + + // Pass Username/Password + transport := &bearerTransport{ + inner: http.DefaultTransport, + bearer: authn.AuthConfig{RegistryToken: initialToken}, + basic: &authn.Basic{Username: "foo", Password: "bar"}, + registry: registry, + realm: server.URL, + scheme: "http", + } + client := http.Client{Transport: transport} + + res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Errorf("Unexpected error during client.Get: %v", err) + return + } + if res.StatusCode != http.StatusOK { + t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK) + } + if got, want := transport.bearer.RegistryToken, refreshedToken; got != want { + t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want) + } + + // Pass RegistryToken directly + transport.bearer = authn.AuthConfig{RegistryToken: initialToken} + transport.basic = &authn.Bearer{Token: refreshedToken} + client = http.Client{Transport: transport} + + res, err = client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Errorf("Unexpected error during client.Get: %v", err) + return + } + if res.StatusCode != http.StatusOK { + t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK) + } + if got, want := transport.bearer.RegistryToken, refreshedToken; got != want { + t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want) + } +} + +func TestBearerTransportOauthRefresh(t *testing.T) { + initialToken := "foo" + accessToken := "bar" + refreshToken := "baz" + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + if err := r.ParseForm(); err != nil { + t.Fatal(err) + } + if it := r.FormValue("refresh_token"); it != initialToken { + t.Errorf("want %s got %s", initialToken, it) + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf(`{"access_token": %q, "refresh_token": %q}`, accessToken, refreshToken))) + return + } + + hdr := r.Header.Get("Authorization") + if hdr == "Bearer "+accessToken { + w.WriteHeader(http.StatusOK) + return + } + + w.Header().Set("WWW-Authenticate", "scope=foo") + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + registry, err := name.NewRegistry(u.Host, name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + transport := &bearerTransport{ + inner: http.DefaultTransport, + basic: authn.FromConfig(authn.AuthConfig{IdentityToken: initialToken}), + registry: registry, + realm: server.URL, + scheme: "http", + scopes: []string{"myscope"}, + service: u.Host, + } + client := http.Client{Transport: transport} + + res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Fatalf("Unexpected error during client.Get: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK) + } + if want, got := transport.bearer.RegistryToken, accessToken; want != got { + t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want) + } + basicAuthConfig, err := transport.basic.Authorization() + if err != nil { + t.Fatal(err) + } + if got, want := basicAuthConfig.IdentityToken, refreshToken; got != want { + t.Errorf("Expected Basic IdentityToken to be refreshed, got %v, want %v", got, want) + } +} + +func TestBearerTransportOauth404Fallback(t *testing.T) { + basicAuth := "basic_auth" + identityToken := "identity_token" + accessToken := "access_token" + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + w.WriteHeader(http.StatusNotFound) + } + + hdr := r.Header.Get("Authorization") + if hdr == "Basic "+basicAuth { + w.WriteHeader(http.StatusOK) + w.Write([]byte(fmt.Sprintf(`{"access_token": %q}`, accessToken))) + } + if hdr == "Bearer "+accessToken { + w.WriteHeader(http.StatusOK) + return + } + + w.Header().Set("WWW-Authenticate", "scope=foo") + w.WriteHeader(http.StatusUnauthorized) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + registry, err := name.NewRegistry(u.Host, name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + transport := &bearerTransport{ + inner: http.DefaultTransport, + basic: authn.FromConfig(authn.AuthConfig{ + IdentityToken: identityToken, + Auth: basicAuth, + }), + registry: registry, + realm: server.URL, + scheme: "http", + scopes: []string{"myscope"}, + service: u.Host, + } + client := http.Client{Transport: transport} + + res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Fatalf("Unexpected error during client.Get: %v", err) + } + if res.StatusCode != http.StatusOK { + t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK) + } + if got, want := transport.bearer.RegistryToken, accessToken; got != want { + t.Errorf("Expected Bearer token to be refreshed, got %v, want %v", got, want) + } +} + +type recorder struct { + reqs []*http.Request + resp *http.Response + err error +} + +func newRecorder(resp *http.Response, err error) *recorder { + return &recorder{ + reqs: []*http.Request{}, + resp: resp, + err: err, + } +} + +func (r *recorder) RoundTrip(in *http.Request) (*http.Response, error) { + r.reqs = append(r.reqs, in) + return r.resp, r.err +} + +func TestSchemeOverride(t *testing.T) { + // Record the requests we get in the inner transport. + cannedResponse := http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + } + rec := newRecorder(&cannedResponse, nil) + registry, err := name.NewRegistry("example.com") + if err != nil { + t.Fatalf("Unexpected error during NewRegistry: %v", err) + } + st := &schemeTransport{ + inner: rec, + registry: registry, + scheme: "http", + } + + // We should see the scheme be overridden to "http" for the registry, but the + // scheme for the token server should be unchanged. + tests := []struct { + url string + wantScheme string + }{{ + url: "https://example.com", + wantScheme: "http", + }, { + url: "https://token.example.com", + wantScheme: "https", + }} + + for i, tt := range tests { + req, err := http.NewRequest("GET", tt.url, nil) + if err != nil { + t.Fatalf("Unexpected error during NewRequest: %v", err) + } + + if _, err := st.RoundTrip(req); err != nil { + t.Fatalf("Unexpected error during RoundTrip: %v", err) + } + + if got, want := rec.reqs[i].URL.Scheme, tt.wantScheme; got != want { + t.Errorf("Wrong scheme: wanted %v, got %v", want, got) + } + } +} + +func TestCanonicalAddressResolution(t *testing.T) { + registry, err := name.NewRegistry("does-not-matter", name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + tests := []struct { + registry name.Registry + scheme string + address string + want string + }{{ + registry: registry, + scheme: "http", + address: "registry.example.com", + want: "registry.example.com:80", + }, { + registry: registry, + scheme: "http", + address: "registry.example.com:12345", + want: "registry.example.com:12345", + }, { + registry: registry, + scheme: "https", + address: "registry.example.com", + want: "registry.example.com:443", + }, { + registry: registry, + scheme: "https", + address: "registry.example.com:12345", + want: "registry.example.com:12345", + }, { + registry: registry, + scheme: "http", + address: "registry.example.com:", + want: "registry.example.com:80", + }, { + registry: registry, + scheme: "https", + address: "registry.example.com:", + want: "registry.example.com:443", + }, { + registry: registry, + scheme: "http", + address: "2001:db8::1", + want: "[2001:db8::1]:80", + }, { + registry: registry, + scheme: "https", + address: "2001:db8::1", + want: "[2001:db8::1]:443", + }, { + registry: registry, + scheme: "http", + address: "[2001:db8::1]:12345", + want: "[2001:db8::1]:12345", + }, { + registry: registry, + scheme: "https", + address: "[2001:db8::1]:12345", + want: "[2001:db8::1]:12345", + }, { + registry: registry, + scheme: "http", + address: "[2001:db8::1]:", + want: "[2001:db8::1]:80", + }, { + registry: registry, + scheme: "https", + address: "[2001:db8::1]:", + want: "[2001:db8::1]:443", + }, { + registry: registry, + scheme: "https", + address: "something:is::wrong]:", + want: "something:is::wrong]:", + }} + + for _, tt := range tests { + got := canonicalAddress(tt.address, tt.scheme) + if got != tt.want { + t.Errorf("Wrong canonical host: wanted %v got %v", tt.want, got) + } + } +} + +func TestInsufficientScope(t *testing.T) { + wrong := "the-wrong-scope" + right := "the-right-scope" + realm := "" + expectedService := "my-service.io" + passed := false + + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + scopes := query["scope"] + switch { + case len(scopes) == 0: + if !passed { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Bearer realm=%q,scope=%q", realm, right)) + w.WriteHeader(http.StatusUnauthorized) + } + case len(scopes) == 1: + w.Write([]byte(`{"token": "arbitrary-token"}`)) + default: + passed = true + w.Write([]byte(`{"token": "arbitrary-token-2"}`)) + } + })) + defer server.Close() + + basic := &authn.Basic{Username: "foo", Password: "bar"} + u, err := url.Parse(server.URL) + if err != nil { + t.Error("Unexpected error during url.Parse: ", err) + } + realm = u.Host + + registry, err := name.NewRegistry(expectedService, name.WeakValidation) + if err != nil { + t.Error("Unexpected error during NewRegistry: ", err) + } + + bt := &bearerTransport{ + inner: http.DefaultTransport, + basic: basic, + registry: registry, + realm: server.URL, + scopes: []string{wrong}, + service: expectedService, + scheme: "http", + } + + client := http.Client{Transport: bt} + + res, err := client.Get(fmt.Sprintf("http://%s/v2/foo/bar/blobs/blah", u.Host)) + if err != nil { + t.Error("Unexpected error during client.Get: ", err) + return + } + if res.StatusCode != http.StatusOK { + t.Errorf("client.Get final StatusCode got %v, want: %v", res.StatusCode, http.StatusOK) + } + + if !passed { + t.Error("didn't refresh insufficient scope") + } +} diff --git a/pkg/v1/remote/transport/doc.go b/pkg/v1/remote/transport/doc.go new file mode 100644 index 0000000..ff7025b --- /dev/null +++ b/pkg/v1/remote/transport/doc.go @@ -0,0 +1,18 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package transport provides facilities for setting up an authenticated +// http.RoundTripper given an Authenticator and base RoundTripper. See +// transport.New for more information. +package transport diff --git a/pkg/v1/remote/transport/error.go b/pkg/v1/remote/transport/error.go new file mode 100644 index 0000000..c0e4337 --- /dev/null +++ b/pkg/v1/remote/transport/error.go @@ -0,0 +1,173 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "github.com/google/go-containerregistry/internal/redact" +) + +// Error implements error to support the following error specification: +// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors +type Error struct { + Errors []Diagnostic `json:"errors,omitempty"` + // The http status code returned. + StatusCode int + // The request that failed. + Request *http.Request + // The raw body if we couldn't understand it. + rawBody string +} + +// Check that Error implements error +var _ error = (*Error)(nil) + +// Error implements error +func (e *Error) Error() string { + prefix := "" + if e.Request != nil { + prefix = fmt.Sprintf("%s %s: ", e.Request.Method, redact.URL(e.Request.URL)) + } + return prefix + e.responseErr() +} + +func (e *Error) responseErr() string { + switch len(e.Errors) { + case 0: + if len(e.rawBody) == 0 { + if e.Request != nil && e.Request.Method == http.MethodHead { + return fmt.Sprintf("unexpected status code %d %s (HEAD responses have no body, use GET for details)", e.StatusCode, http.StatusText(e.StatusCode)) + } + return fmt.Sprintf("unexpected status code %d %s", e.StatusCode, http.StatusText(e.StatusCode)) + } + return fmt.Sprintf("unexpected status code %d %s: %s", e.StatusCode, http.StatusText(e.StatusCode), e.rawBody) + case 1: + return e.Errors[0].String() + default: + var errors []string + for _, d := range e.Errors { + errors = append(errors, d.String()) + } + return fmt.Sprintf("multiple errors returned: %s", + strings.Join(errors, "; ")) + } +} + +// Temporary returns whether the request that preceded the error is temporary. +func (e *Error) Temporary() bool { + if len(e.Errors) == 0 { + _, ok := temporaryStatusCodes[e.StatusCode] + return ok + } + for _, d := range e.Errors { + if _, ok := temporaryErrorCodes[d.Code]; !ok { + return false + } + } + return true +} + +// Diagnostic represents a single error returned by a Docker registry interaction. +type Diagnostic struct { + Code ErrorCode `json:"code"` + Message string `json:"message,omitempty"` + Detail any `json:"detail,omitempty"` +} + +// String stringifies the Diagnostic in the form: $Code: $Message[; $Detail] +func (d Diagnostic) String() string { + msg := fmt.Sprintf("%s: %s", d.Code, d.Message) + if d.Detail != nil { + msg = fmt.Sprintf("%s; %v", msg, d.Detail) + } + return msg +} + +// ErrorCode is an enumeration of supported error codes. +type ErrorCode string + +// The set of error conditions a registry may return: +// https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors-2 +const ( + BlobUnknownErrorCode ErrorCode = "BLOB_UNKNOWN" + BlobUploadInvalidErrorCode ErrorCode = "BLOB_UPLOAD_INVALID" + BlobUploadUnknownErrorCode ErrorCode = "BLOB_UPLOAD_UNKNOWN" + DigestInvalidErrorCode ErrorCode = "DIGEST_INVALID" + ManifestBlobUnknownErrorCode ErrorCode = "MANIFEST_BLOB_UNKNOWN" + ManifestInvalidErrorCode ErrorCode = "MANIFEST_INVALID" + ManifestUnknownErrorCode ErrorCode = "MANIFEST_UNKNOWN" + ManifestUnverifiedErrorCode ErrorCode = "MANIFEST_UNVERIFIED" + NameInvalidErrorCode ErrorCode = "NAME_INVALID" + NameUnknownErrorCode ErrorCode = "NAME_UNKNOWN" + SizeInvalidErrorCode ErrorCode = "SIZE_INVALID" + TagInvalidErrorCode ErrorCode = "TAG_INVALID" + UnauthorizedErrorCode ErrorCode = "UNAUTHORIZED" + DeniedErrorCode ErrorCode = "DENIED" + UnsupportedErrorCode ErrorCode = "UNSUPPORTED" + TooManyRequestsErrorCode ErrorCode = "TOOMANYREQUESTS" + UnknownErrorCode ErrorCode = "UNKNOWN" + + // This isn't defined by either docker or OCI spec, but is defined by docker/distribution: + // https://github.com/distribution/distribution/blob/6a977a5a754baa213041443f841705888107362a/registry/api/errcode/register.go#L60 + UnavailableErrorCode ErrorCode = "UNAVAILABLE" +) + +// TODO: Include other error types. +var temporaryErrorCodes = map[ErrorCode]struct{}{ + BlobUploadInvalidErrorCode: {}, + TooManyRequestsErrorCode: {}, + UnknownErrorCode: {}, + UnavailableErrorCode: {}, +} + +var temporaryStatusCodes = map[int]struct{}{ + http.StatusRequestTimeout: {}, + http.StatusInternalServerError: {}, + http.StatusBadGateway: {}, + http.StatusServiceUnavailable: {}, + http.StatusGatewayTimeout: {}, +} + +// CheckError returns a structured error if the response status is not in codes. +func CheckError(resp *http.Response, codes ...int) error { + for _, code := range codes { + if resp.StatusCode == code { + // This is one of the supported status codes. + return nil + } + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + // https://github.com/docker/distribution/blob/master/docs/spec/api.md#errors + structuredError := &Error{} + + // This can fail if e.g. the response body is not valid JSON. That's fine, + // we'll construct an appropriate error string from the body and status code. + _ = json.Unmarshal(b, structuredError) + + structuredError.rawBody = string(b) + structuredError.StatusCode = resp.StatusCode + structuredError.Request = resp.Request + + return structuredError +} diff --git a/pkg/v1/remote/transport/error_test.go b/pkg/v1/remote/transport/error_test.go new file mode 100644 index 0000000..e42ce3a --- /dev/null +++ b/pkg/v1/remote/transport/error_test.go @@ -0,0 +1,236 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "bytes" + "errors" + "io" + "net/http" + "net/url" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestTemporary(t *testing.T) { + tests := []struct { + error *Error + retry bool + }{{ + error: &Error{}, + retry: false, + }, { + error: &Error{ + Errors: []Diagnostic{{ + Code: BlobUploadInvalidErrorCode, + }}, + }, + retry: true, + }, { + error: &Error{ + Errors: []Diagnostic{{ + Code: BlobUploadInvalidErrorCode, + }, { + Code: DeniedErrorCode, + }}, + }, + retry: false, + }, { + error: &Error{ + Errors: []Diagnostic{{ + Code: TooManyRequestsErrorCode, + }}, + }, + retry: true, + }, { + error: &Error{ + Errors: []Diagnostic{{ + Code: UnavailableErrorCode, + }}, + }, + retry: true, + }, { + error: &Error{ + StatusCode: http.StatusInternalServerError, + }, + retry: true, + }} + + for _, test := range tests { + retry := test.error.Temporary() + + if test.retry != retry { + t.Errorf("Temporary(%s) = %t, wanted %t", test.error, retry, test.retry) + } + } +} + +func TestCheckErrorNil(t *testing.T) { + tests := []int{ + http.StatusOK, + http.StatusAccepted, + http.StatusCreated, + http.StatusMovedPermanently, + http.StatusInternalServerError, + } + + for _, code := range tests { + resp := &http.Response{StatusCode: code} + + if err := CheckError(resp, code); err != nil { + t.Errorf("CheckError(%d) = %v", code, err) + } + } +} + +func TestCheckErrorNotError(t *testing.T) { + tests := []struct { + code int + body string + msg string + request *http.Request + }{{ + code: http.StatusBadRequest, + body: "", + msg: "unexpected status code 400 Bad Request", + }, { + code: http.StatusUnauthorized, + // Valid JSON, but not a structured error -- we should still print the body. + body: `{"details":"incorrect username or password"}`, + msg: `unexpected status code 401 Unauthorized: {"details":"incorrect username or password"}`, + }, { + code: http.StatusUnauthorized, + body: "Not JSON", + msg: "GET https://example.com/somepath?access_token=REDACTED&scope=foo&service=bar: unexpected status code 401 Unauthorized: Not JSON", + request: &http.Request{ + Method: http.MethodGet, + URL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "somepath", + RawQuery: url.Values{ + "scope": []string{"foo"}, + "service": []string{"bar"}, + "access_token": []string{"hunter2"}, + }.Encode(), + }, + }, + }, { + code: http.StatusUnauthorized, + body: "", + msg: "HEAD https://example.com/somepath: unexpected status code 401 Unauthorized (HEAD responses have no body, use GET for details)", + request: &http.Request{ + Method: http.MethodHead, + URL: &url.URL{ + Scheme: "https", + Host: "example.com", + Path: "somepath", + }, + }, + }} + + for _, test := range tests { + resp := &http.Response{ + StatusCode: test.code, + Body: io.NopCloser(bytes.NewBufferString(test.body)), + Request: test.request, + } + + err := CheckError(resp, http.StatusOK) + if err == nil { + t.Fatalf("CheckError(%d, %s) = nil, wanted error", test.code, test.body) + } + var terr *Error + if !errors.As(err, &terr) { + t.Fatalf("CheckError(%d, %s) = %v, wanted error type", test.code, test.body, err) + } + + if terr.StatusCode != test.code { + t.Errorf("Incorrect status code, got %d, want %d", terr.StatusCode, test.code) + } + + if terr.Error() != test.msg { + t.Errorf("Incorrect message, got %q, want %q", terr.Error(), test.msg) + } + } +} + +func TestCheckErrorWithError(t *testing.T) { + tests := []struct { + name string + code int + errorBody string + msg string + }{{ + name: "Invalid name error", + code: http.StatusBadRequest, + errorBody: `{"errors":[{"code":"NAME_INVALID","message":"a message for you"}],"StatusCode":400}`, + msg: "NAME_INVALID: a message for you", + }, { + name: "Only status code is provided", + code: http.StatusBadRequest, + errorBody: `{"StatusCode":400}`, + msg: "unexpected status code 400 Bad Request: {\"StatusCode\":400}", + }, { + name: "Multiple diagnostics", + code: http.StatusBadRequest, + errorBody: `{"errors":[{"code":"NAME_INVALID","message":"a message for you"}, {"code":"SIZE_INVALID","message":"another message for you", "detail": "with some details"}],"StatusCode":400,"Request":null}`, + msg: "multiple errors returned: NAME_INVALID: a message for you; SIZE_INVALID: another message for you; with some details", + }} + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resp := &http.Response{ + StatusCode: test.code, + Body: io.NopCloser(bytes.NewBuffer([]byte(test.errorBody))), + } + + var terr *Error + if err := CheckError(resp, http.StatusOK); err == nil { + t.Errorf("CheckError(%d, %s) = nil, wanted error", test.code, test.errorBody) + } else if !errors.As(err, &terr) { + t.Errorf("CheckError(%d, %s) = %T, wanted *transport.Error", test.code, test.errorBody, err) + } else if diff := cmp.Diff(test.msg, err.Error()); diff != "" { + t.Errorf("CheckError(%d, %s).Error(); (-want +got) %s", test.code, test.errorBody, diff) + } + }) + } +} + +func TestBodyError(t *testing.T) { + expectedErr := errors.New("whoops") + resp := &http.Response{ + StatusCode: http.StatusOK, + Body: &errReadCloser{expectedErr}, + } + if err := CheckError(resp, http.StatusNotFound); err == nil { + t.Errorf("CheckError() = nil, wanted error %v", expectedErr) + } else if !errors.Is(err, expectedErr) { + t.Errorf("CheckError() = %v, wanted %v", err, expectedErr) + } +} + +type errReadCloser struct { + err error +} + +func (e *errReadCloser) Read(p []byte) (int, error) { + return 0, e.err +} + +func (e *errReadCloser) Close() error { + return e.err +} diff --git a/pkg/v1/remote/transport/logger.go b/pkg/v1/remote/transport/logger.go new file mode 100644 index 0000000..c341f84 --- /dev/null +++ b/pkg/v1/remote/transport/logger.go @@ -0,0 +1,91 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "fmt" + "net/http" + "net/http/httputil" + "time" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/pkg/logs" +) + +type logTransport struct { + inner http.RoundTripper +} + +// NewLogger returns a transport that logs requests and responses to +// github.com/google/go-containerregistry/pkg/logs.Debug. +func NewLogger(inner http.RoundTripper) http.RoundTripper { + return &logTransport{inner} +} + +func (t *logTransport) RoundTrip(in *http.Request) (out *http.Response, err error) { + // Inspired by: github.com/motemen/go-loghttp + + // We redact token responses and binary blobs in response/request. + omitBody, reason := redact.FromContext(in.Context()) + if omitBody { + logs.Debug.Printf("--> %s %s [body redacted: %s]", in.Method, in.URL, reason) + } else { + logs.Debug.Printf("--> %s %s", in.Method, in.URL) + } + + // Save these headers so we can redact Authorization. + savedHeaders := in.Header.Clone() + if in.Header != nil && in.Header.Get("authorization") != "" { + in.Header.Set("authorization", "<redacted>") + } + + b, err := httputil.DumpRequestOut(in, !omitBody) + if err == nil { + logs.Debug.Println(string(b)) + } else { + logs.Debug.Printf("Failed to dump request %s %s: %v", in.Method, in.URL, err) + } + + // Restore the non-redacted headers. + in.Header = savedHeaders + + start := time.Now() + out, err = t.inner.RoundTrip(in) + duration := time.Since(start) + if err != nil { + logs.Debug.Printf("<-- %v %s %s (%s)", err, in.Method, in.URL, duration) + } + if out != nil { + msg := fmt.Sprintf("<-- %d", out.StatusCode) + if out.Request != nil { + msg = fmt.Sprintf("%s %s", msg, out.Request.URL) + } + msg = fmt.Sprintf("%s (%s)", msg, duration) + + if omitBody { + msg = fmt.Sprintf("%s [body redacted: %s]", msg, reason) + } + + logs.Debug.Print(msg) + + b, err := httputil.DumpResponse(out, !omitBody) + if err == nil { + logs.Debug.Println(string(b)) + } else { + logs.Debug.Printf("Failed to dump response %s %s: %v", in.Method, in.URL, err) + } + } + return +} diff --git a/pkg/v1/remote/transport/logger_test.go b/pkg/v1/remote/transport/logger_test.go new file mode 100644 index 0000000..d5b57d3 --- /dev/null +++ b/pkg/v1/remote/transport/logger_test.go @@ -0,0 +1,93 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/pkg/logs" +) + +func TestLogger(t *testing.T) { + canary := "logs.Debug canary" + secret := "super secret do not log" + auth := "my token pls do not log" + reason := "should not log the secret" + + ctx := redact.NewContext(context.Background(), reason) + + req, err := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil) + if err != nil { + t.Fatalf("Unexpected error during NewRequest: %v", err) + } + req.Header.Set("authorization", auth) + + var b bytes.Buffer + logs.Debug.SetOutput(&b) + cannedResponse := http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Header: http.Header{ + "Foo": []string{canary}, + }, + Body: io.NopCloser(strings.NewReader(secret)), + Request: req, + } + tr := NewLogger(newRecorder(&cannedResponse, nil)) + if _, err := tr.RoundTrip(req); err != nil { + t.Fatalf("Unexpected error during RoundTrip: %v", err) + } + + logged := b.String() + if !strings.Contains(logged, canary) { + t.Errorf("Expected logs to contain %s, got %s", canary, logged) + } + if !strings.Contains(logged, reason) { + t.Errorf("Expected logs to contain %s, got %s", canary, logged) + } + if strings.Contains(logged, secret) { + t.Errorf("Expected logs NOT to contain %s, got %s", secret, logged) + } + if strings.Contains(logged, auth) { + t.Errorf("Expected logs NOT to contain %s, got %s", auth, logged) + } +} + +func TestLoggerError(t *testing.T) { + canary := "logs.Debug canary ERROR" + req, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Fatalf("Unexpected error during NewRequest: %v", err) + } + + var b bytes.Buffer + logs.Debug.SetOutput(&b) + tr := NewLogger(newRecorder(nil, errors.New(canary))) + if _, err := tr.RoundTrip(req); err == nil { + t.Fatalf("Expected error during RoundTrip, got nil") + } + + logged := b.String() + if !strings.Contains(logged, canary) { + t.Errorf("Expected logs to contain %s, got %s", canary, logged) + } +} diff --git a/pkg/v1/remote/transport/ping.go b/pkg/v1/remote/transport/ping.go new file mode 100644 index 0000000..d852ef8 --- /dev/null +++ b/pkg/v1/remote/transport/ping.go @@ -0,0 +1,227 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + authchallenge "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" +) + +type challenge string + +const ( + anonymous challenge = "anonymous" + basic challenge = "basic" + bearer challenge = "bearer" +) + +// 300ms is the default fallback period for go's DNS dialer but we could make this configurable. +var fallbackDelay = 300 * time.Millisecond + +type pingResp struct { + challenge challenge + + // Following the challenge there are often key/value pairs + // e.g. Bearer service="gcr.io",realm="https://auth.gcr.io/v36/tokenz" + parameters map[string]string + + // The registry's scheme to use. Communicates whether we fell back to http. + scheme string +} + +func (c challenge) Canonical() challenge { + return challenge(strings.ToLower(string(c))) +} + +func ping(ctx context.Context, reg name.Registry, t http.RoundTripper) (*pingResp, error) { + // This first attempts to use "https" for every request, falling back to http + // if the registry matches our localhost heuristic or if it is intentionally + // set to insecure via name.NewInsecureRegistry. + schemes := []string{"https"} + if reg.Scheme() == "http" { + schemes = append(schemes, "http") + } + if len(schemes) == 1 { + return pingSingle(ctx, reg, t, schemes[0]) + } + return pingParallel(ctx, reg, t, schemes) +} + +func pingSingle(ctx context.Context, reg name.Registry, t http.RoundTripper, scheme string) (*pingResp, error) { + client := http.Client{Transport: t} + url := fmt.Sprintf("%s://%s/v2/", scheme, reg.Name()) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer func() { + // By draining the body, make sure to reuse the connection made by + // the ping for the following access to the registry + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + // If we get a 200, then no authentication is needed. + return &pingResp{ + challenge: anonymous, + scheme: scheme, + }, nil + case http.StatusUnauthorized: + if challenges := authchallenge.ResponseChallenges(resp); len(challenges) != 0 { + // If we hit more than one, let's try to find one that we know how to handle. + wac := pickFromMultipleChallenges(challenges) + return &pingResp{ + challenge: challenge(wac.Scheme).Canonical(), + parameters: wac.Parameters, + scheme: scheme, + }, nil + } + // Otherwise, just return the challenge without parameters. + return &pingResp{ + challenge: challenge(resp.Header.Get("WWW-Authenticate")).Canonical(), + scheme: scheme, + }, nil + default: + return nil, CheckError(resp, http.StatusOK, http.StatusUnauthorized) + } +} + +// Based on the golang happy eyeballs dialParallel impl in net/dial.go. +func pingParallel(ctx context.Context, reg name.Registry, t http.RoundTripper, schemes []string) (*pingResp, error) { + returned := make(chan struct{}) + defer close(returned) + + type pingResult struct { + *pingResp + error + primary bool + done bool + } + + results := make(chan pingResult) + + startRacer := func(ctx context.Context, scheme string) { + pr, err := pingSingle(ctx, reg, t, scheme) + select { + case results <- pingResult{pingResp: pr, error: err, primary: scheme == "https", done: true}: + case <-returned: + if pr != nil { + logs.Debug.Printf("%s lost race", scheme) + } + } + } + + var primary, fallback pingResult + + primaryCtx, primaryCancel := context.WithCancel(ctx) + defer primaryCancel() + go startRacer(primaryCtx, schemes[0]) + + fallbackTimer := time.NewTimer(fallbackDelay) + defer fallbackTimer.Stop() + + for { + select { + case <-fallbackTimer.C: + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + go startRacer(fallbackCtx, schemes[1]) + + case res := <-results: + if res.error == nil { + return res.pingResp, nil + } + if res.primary { + primary = res + } else { + fallback = res + } + if primary.done && fallback.done { + return nil, multierrs([]error{primary.error, fallback.error}) + } + if res.primary && fallbackTimer.Stop() { + // Primary failed and we haven't started the fallback, + // reset time to start fallback immediately. + fallbackTimer.Reset(0) + } + } + } +} + +func pickFromMultipleChallenges(challenges []authchallenge.Challenge) authchallenge.Challenge { + // It might happen there are multiple www-authenticate headers, e.g. `Negotiate` and `Basic`. + // Picking simply the first one could result eventually in `unrecognized challenge` error, + // that's why we're looping through the challenges in search for one that can be handled. + allowedSchemes := []string{"basic", "bearer"} + + for _, wac := range challenges { + currentScheme := strings.ToLower(wac.Scheme) + for _, allowed := range allowedSchemes { + if allowed == currentScheme { + return wac + } + } + } + + return challenges[0] +} + +type multierrs []error + +func (m multierrs) Error() string { + var b strings.Builder + hasWritten := false + for _, err := range m { + if hasWritten { + b.WriteString("; ") + } + hasWritten = true + b.WriteString(err.Error()) + } + return b.String() +} + +func (m multierrs) As(target any) bool { + for _, err := range m { + if errors.As(err, target) { + return true + } + } + return false +} + +func (m multierrs) Is(target error) bool { + for _, err := range m { + if errors.Is(err, target) { + return true + } + } + return false +} diff --git a/pkg/v1/remote/transport/ping_test.go b/pkg/v1/remote/transport/ping_test.go new file mode 100644 index 0000000..c2ad119 --- /dev/null +++ b/pkg/v1/remote/transport/ping_test.go @@ -0,0 +1,260 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/name" +) + +var ( + testRegistry, _ = name.NewRegistry("localhost:8080", name.StrictValidation) +) + +func TestPingNoChallenge(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err != nil { + t.Errorf("ping() = %v", err) + } + if pr.challenge != anonymous { + t.Errorf("ping(); got %v, want %v", pr.challenge, anonymous) + } + if pr.scheme != "http" { + t.Errorf("ping(); got %v, want %v", pr.scheme, "http") + } +} + +func TestPingBasicChallengeNoParams(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `BASIC`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err != nil { + t.Errorf("ping() = %v", err) + } + if pr.challenge != basic { + t.Errorf("ping(); got %v, want %v", pr.challenge, basic) + } + if got, want := len(pr.parameters), 0; got != want { + t.Errorf("ping(); got %v, want %v", got, want) + } +} + +func TestPingBearerChallengeWithParams(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err != nil { + t.Errorf("ping() = %v", err) + } + if pr.challenge != bearer { + t.Errorf("ping(); got %v, want %v", pr.challenge, bearer) + } + if got, want := len(pr.parameters), 1; got != want { + t.Errorf("ping(); got %v, want %v", got, want) + } +} + +func TestPingMultipleChallenges(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("WWW-Authenticate", "Negotiate") + w.Header().Add("WWW-Authenticate", `Basic realm="http://auth.example.com/token"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err != nil { + t.Errorf("ping() = %v", err) + } + if pr.challenge != basic { + t.Errorf("ping(); got %v, want %v", pr.challenge, basic) + } + if got, want := len(pr.parameters), 1; got != want { + t.Errorf("ping(); got %v, want %v", got, want) + } +} + +func TestPingMultipleNotSupportedChallenges(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("WWW-Authenticate", "Negotiate") + w.Header().Add("WWW-Authenticate", "Digest") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err != nil { + t.Errorf("ping() = %v", err) + } + if pr.challenge != "negotiate" { + t.Errorf("ping(); got %v, want %v", pr.challenge, "negotiate") + } +} + +func TestUnsupportedStatus(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer realm="http://auth.example.com/token`) + http.Error(w, "Forbidden", http.StatusForbidden) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + pr, err := ping(context.Background(), testRegistry, tprt) + if err == nil { + t.Errorf("ping() = %v", pr) + } +} + +func TestPingHttpFallback(t *testing.T) { + tests := []struct { + reg name.Registry + wantCount int64 + err string + contains []string + }{{ + reg: mustRegistry("gcr.io"), + wantCount: 1, + err: `Get "https://gcr.io/v2/": http: server gave HTTP response to HTTPS client`, + }, { + reg: mustRegistry("ko.local"), + wantCount: 2, + }, { + reg: mustInsecureRegistry("us.gcr.io"), + wantCount: 0, + contains: []string{"https://us.gcr.io/v2/", "http://us.gcr.io/v2/"}, + }} + + gotCount := int64(0) + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt64(&gotCount, 1) + if r.URL.Scheme != "http" { + // Sleep a little bit so we can exercise the + // happy eyeballs race. + time.Sleep(5 * time.Millisecond) + } + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + fallbackDelay = 2 * time.Millisecond + + for _, test := range tests { + // This is the last one, fatal error it. + if strings.Contains(test.reg.String(), "us.gcr.io") { + server.Close() + } + + _, err := ping(context.Background(), test.reg, tprt) + if got, want := gotCount, test.wantCount; got != want { + t.Errorf("%s: got %d requests, wanted %d", test.reg.String(), got, want) + } + gotCount = 0 + + if err == nil { + if test.err != "" { + t.Error("expected err, got nil") + } + continue + } + if len(test.contains) != 0 { + for _, c := range test.contains { + if !strings.Contains(err.Error(), c) { + t.Errorf("expected err to contain %q but did not: %q", c, err) + } + } + } else if got, want := err.Error(), test.err; got != want { + t.Errorf("got %q want %q", got, want) + } + } +} + +func mustRegistry(r string) name.Registry { + reg, err := name.NewRegistry(r) + if err != nil { + panic(err) + } + return reg +} + +func mustInsecureRegistry(r string) name.Registry { + reg, err := name.NewRegistry(r, name.Insecure) + if err != nil { + panic(err) + } + return reg +} diff --git a/pkg/v1/remote/transport/retry.go b/pkg/v1/remote/transport/retry.go new file mode 100644 index 0000000..e5621e3 --- /dev/null +++ b/pkg/v1/remote/transport/retry.go @@ -0,0 +1,111 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "net/http" + "time" + + "github.com/google/go-containerregistry/internal/retry" +) + +// Sleep for 0.1 then 0.3 seconds. This should cover networking blips. +var defaultBackoff = retry.Backoff{ + Duration: 100 * time.Millisecond, + Factor: 3.0, + Jitter: 0.1, + Steps: 3, +} + +var _ http.RoundTripper = (*retryTransport)(nil) + +// retryTransport wraps a RoundTripper and retries temporary network errors. +type retryTransport struct { + inner http.RoundTripper + backoff retry.Backoff + predicate retry.Predicate + codes []int +} + +// Option is a functional option for retryTransport. +type Option func(*options) + +type options struct { + backoff retry.Backoff + predicate retry.Predicate + codes []int +} + +// Backoff is an alias of retry.Backoff to expose this configuration option to consumers of this lib +type Backoff = retry.Backoff + +// WithRetryBackoff sets the backoff for retry operations. +func WithRetryBackoff(backoff Backoff) Option { + return func(o *options) { + o.backoff = backoff + } +} + +// WithRetryPredicate sets the predicate for retry operations. +func WithRetryPredicate(predicate func(error) bool) Option { + return func(o *options) { + o.predicate = predicate + } +} + +// WithRetryStatusCodes sets which http response codes will be retried. +func WithRetryStatusCodes(codes ...int) Option { + return func(o *options) { + o.codes = codes + } +} + +// NewRetry returns a transport that retries errors. +func NewRetry(inner http.RoundTripper, opts ...Option) http.RoundTripper { + o := &options{ + backoff: defaultBackoff, + predicate: retry.IsTemporary, + } + + for _, opt := range opts { + opt(o) + } + + return &retryTransport{ + inner: inner, + backoff: o.backoff, + predicate: o.predicate, + codes: o.codes, + } +} + +func (t *retryTransport) RoundTrip(in *http.Request) (out *http.Response, err error) { + roundtrip := func() error { + out, err = t.inner.RoundTrip(in) + if !retry.Ever(in.Context()) { + return nil + } + if out != nil { + for _, code := range t.codes { + if out.StatusCode == code { + return CheckError(out) + } + } + } + return err + } + retry.Retry(roundtrip, t.predicate, t.backoff) + return +} diff --git a/pkg/v1/remote/transport/retry_test.go b/pkg/v1/remote/transport/retry_test.go new file mode 100644 index 0000000..ded0ce0 --- /dev/null +++ b/pkg/v1/remote/transport/retry_test.go @@ -0,0 +1,177 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/google/go-containerregistry/internal/retry" +) + +type mockTransport struct { + errs []error + resps []*http.Response + count int +} + +func (t *mockTransport) RoundTrip(in *http.Request) (out *http.Response, err error) { + defer func() { t.count++ }() + if t.count < len(t.resps) { + out = t.resps[t.count] + } + if t.count < len(t.errs) { + err = t.errs[t.count] + } + return +} + +type perm struct{} + +func (e perm) Error() string { + return "permanent error" +} + +type temp struct{} + +func (e temp) Error() string { + return "temporary error" +} + +func (e temp) Temporary() bool { + return true +} + +func resp(code int) *http.Response { + return &http.Response{ + StatusCode: code, + Body: io.NopCloser(strings.NewReader("hi")), + } +} + +func TestRetryTransport(t *testing.T) { + for _, test := range []struct { + errs []error + resps []*http.Response + ctx context.Context + count int + }{{ + // Don't retry retry.Never. + errs: []error{temp{}}, + ctx: retry.Never(context.Background()), + count: 1, + }, { + // Don't retry permanent. + errs: []error{perm{}}, + count: 1, + }, { + // Do retry temp. + errs: []error{temp{}, perm{}}, + count: 2, + }, { + // Stop at some max. + errs: []error{temp{}, temp{}, temp{}, temp{}, temp{}}, + count: 3, + }, { + // Retry http errors. + errs: []error{nil, nil, temp{}, temp{}, temp{}}, + resps: []*http.Response{ + resp(http.StatusRequestTimeout), + resp(http.StatusInternalServerError), + nil, + }, + count: 3, + }} { + mt := mockTransport{ + errs: test.errs, + resps: test.resps, + } + + tr := NewRetry(&mt, + WithRetryBackoff(retry.Backoff{Steps: 3}), + WithRetryPredicate(retry.IsTemporary), + WithRetryStatusCodes(http.StatusRequestTimeout, http.StatusInternalServerError), + ) + + ctx := context.Background() + if test.ctx != nil { + ctx = test.ctx + } + req, err := http.NewRequestWithContext(ctx, "GET", "example.com", nil) + if err != nil { + t.Fatal(err) + } + tr.RoundTrip(req) + if mt.count != test.count { + t.Errorf("wrong count, wanted %d, got %d", test.count, mt.count) + } + } +} + +func TestRetryDefaults(t *testing.T) { + tr := NewRetry(http.DefaultTransport) + rt, ok := tr.(*retryTransport) + if !ok { + t.Fatal("could not cast to retryTransport") + } + + if rt.backoff != defaultBackoff { + t.Fatalf("default backoff wrong: %v", rt.backoff) + } + + if rt.predicate == nil { + t.Fatal("default predicate not set") + } +} + +func TestTimeoutContext(t *testing.T) { + tr := NewRetry(http.DefaultTransport) + + slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // hanging request + time.Sleep(time.Second * 1) + })) + defer func() { go func() { slowServer.Close() }() }() + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond*20)) + defer cancel() + req, err := http.NewRequest("GET", slowServer.URL, nil) + if err != nil { + t.Fatal(err) + } + req = req.WithContext(ctx) + + result := make(chan error) + + go func() { + _, err := tr.RoundTrip(req) + result <- err + }() + + select { + case err := <-result: + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("got: %v, want: %v", err, context.DeadlineExceeded) + } + case <-time.After(time.Millisecond * 100): + t.Fatalf("deadline was not recognized by transport") + } +} diff --git a/pkg/v1/remote/transport/schemer.go b/pkg/v1/remote/transport/schemer.go new file mode 100644 index 0000000..d70b6a8 --- /dev/null +++ b/pkg/v1/remote/transport/schemer.go @@ -0,0 +1,44 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "net/http" + + "github.com/google/go-containerregistry/pkg/name" +) + +type schemeTransport struct { + // Scheme we should use, determined by ping response. + scheme string + + // Registry we're talking to. + registry name.Registry + + // Wrapped by schemeTransport. + inner http.RoundTripper +} + +// RoundTrip implements http.RoundTripper +func (st *schemeTransport) RoundTrip(in *http.Request) (*http.Response, error) { + // When we ping() the registry, we determine whether to use http or https + // based on which scheme was successful. That is only valid for the + // registry server and not e.g. a separate token server or blob storage, + // so we should only override the scheme if the host is the registry. + if matchesHost(st.registry, in, st.scheme) { + in.URL.Scheme = st.scheme + } + return st.inner.RoundTrip(in) +} diff --git a/pkg/v1/remote/transport/scope.go b/pkg/v1/remote/transport/scope.go new file mode 100644 index 0000000..c3b56f7 --- /dev/null +++ b/pkg/v1/remote/transport/scope.go @@ -0,0 +1,24 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +// Scopes suitable to qualify each Repository +const ( + PullScope string = "pull" + PushScope string = "push,pull" + // For now DELETE is PUSH, which is the read/write ACL. + DeleteScope string = PushScope + CatalogScope string = "catalog" +) diff --git a/pkg/v1/remote/transport/transport.go b/pkg/v1/remote/transport/transport.go new file mode 100644 index 0000000..01fe1fa --- /dev/null +++ b/pkg/v1/remote/transport/transport.go @@ -0,0 +1,116 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" +) + +// New returns a new RoundTripper based on the provided RoundTripper that has been +// setup to authenticate with the remote registry "reg", in the capacity +// laid out by the specified scopes. +// +// Deprecated: Use NewWithContext. +func New(reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) { + return NewWithContext(context.Background(), reg, auth, t, scopes) +} + +// NewWithContext returns a new RoundTripper based on the provided RoundTripper that has been +// set up to authenticate with the remote registry "reg", in the capacity +// laid out by the specified scopes. +// In case the RoundTripper is already of the type Wrapper it assumes +// authentication was already done prior to this call, so it just returns +// the provided RoundTripper without further action +func NewWithContext(ctx context.Context, reg name.Registry, auth authn.Authenticator, t http.RoundTripper, scopes []string) (http.RoundTripper, error) { + // When the transport provided is of the type Wrapper this function assumes that the caller already + // executed the necessary login and check. + switch t.(type) { + case *Wrapper: + return t, nil + } + // The handshake: + // 1. Use "t" to ping() the registry for the authentication challenge. + // + // 2a. If we get back a 200, then simply use "t". + // + // 2b. If we get back a 401 with a Basic challenge, then use a transport + // that just attachs auth each roundtrip. + // + // 2c. If we get back a 401 with a Bearer challenge, then use a transport + // that attaches a bearer token to each request, and refreshes is on 401s. + // Perform an initial refresh to seed the bearer token. + + // First we ping the registry to determine the parameters of the authentication handshake + // (if one is even necessary). + pr, err := ping(ctx, reg, t) + if err != nil { + return nil, err + } + + // Wrap t with a useragent transport unless we already have one. + if _, ok := t.(*userAgentTransport); !ok { + t = NewUserAgent(t, "") + } + + // Wrap t in a transport that selects the appropriate scheme based on the ping response. + t = &schemeTransport{ + scheme: pr.scheme, + registry: reg, + inner: t, + } + + switch pr.challenge.Canonical() { + case anonymous, basic: + return &Wrapper{&basicTransport{inner: t, auth: auth, target: reg.RegistryStr()}}, nil + case bearer: + // We require the realm, which tells us where to send our Basic auth to turn it into Bearer auth. + realm, ok := pr.parameters["realm"] + if !ok { + return nil, fmt.Errorf("malformed www-authenticate, missing realm: %v", pr.parameters) + } + service := pr.parameters["service"] + bt := &bearerTransport{ + inner: t, + basic: auth, + realm: realm, + registry: reg, + service: service, + scopes: scopes, + scheme: pr.scheme, + } + if err := bt.refresh(ctx); err != nil { + return nil, err + } + return &Wrapper{bt}, nil + default: + return nil, fmt.Errorf("unrecognized challenge: %s", pr.challenge) + } +} + +// Wrapper results in *not* wrapping supplied transport with additional logic such as retries, useragent and debug logging +// Consumers are opt-ing into providing their own transport without any additional wrapping. +type Wrapper struct { + inner http.RoundTripper +} + +// RoundTrip delegates to the inner RoundTripper +func (w *Wrapper) RoundTrip(in *http.Request) (*http.Response, error) { + return w.inner.RoundTrip(in) +} diff --git a/pkg/v1/remote/transport/transport_test.go b/pkg/v1/remote/transport/transport_test.go new file mode 100644 index 0000000..10389b7 --- /dev/null +++ b/pkg/v1/remote/transport/transport_test.go @@ -0,0 +1,282 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" +) + +var ( + testReference, _ = name.NewTag("localhost:8080/user/image:latest", name.StrictValidation) +) + +func TestTransportNoActionIfTransportIsAlreadyWrapper(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`) + http.Error(w, "Should not contact the server", http.StatusBadRequest) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + wTprt := &Wrapper{inner: tprt} + + if _, err := NewWithContext(context.Background(), testReference.Context().Registry, nil, wTprt, []string{testReference.Scope(PullScope)}); err != nil { + t.Errorf("NewWithContext unexpected error %s", err) + } +} + +func TestTransportSelectionAnonymous(t *testing.T) { + // Record the requests we get in the inner transport. + cannedResponse := http.Response{ + Status: http.StatusText(http.StatusOK), + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("")), + } + recorder := newRecorder(&cannedResponse, nil) + + basic := &authn.Basic{Username: "foo", Password: "bar"} + reg := testReference.Context().Registry + + tp, err := NewWithContext(context.Background(), reg, basic, recorder, []string{testReference.Scope(PullScope)}) + if err != nil { + t.Errorf("NewWithContext() = %v", err) + } + + req, err := http.NewRequest("GET", fmt.Sprintf("http://%s/v2/anything", reg), nil) + if err != nil { + t.Fatalf("Unexpected error during NewRequest: %v", err) + } + if _, err := tp.RoundTrip(req); err != nil { + t.Fatalf("Unexpected error during RoundTrip: %v", err) + } + + if got, want := len(recorder.reqs), 2; got != want { + t.Fatalf("expected %d requests, got %d", want, got) + } + recorded := recorder.reqs[1] + if got, want := recorded.URL.Scheme, "https"; got != want { + t.Errorf("wrong scheme, want %s got %s", want, got) + } +} + +func TestTransportSelectionBasic(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Basic`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + + tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) + if err != nil { + t.Errorf("NewWithContext() = %v", err) + } + if tpw, ok := tp.(*Wrapper); !ok { + t.Errorf("NewWithContext(); got %T, want *Wrapper", tp) + } else if _, ok := tpw.inner.(*basicTransport); !ok { + t.Errorf("NewWithContext(); got %T, want *basicTransport", tp) + } +} + +type badAuth struct{} + +func (a *badAuth) Authorization() (*authn.AuthConfig, error) { + return nil, errors.New("sorry dave, I'm afraid I can't let you do that") +} + +func TestTransportBadAuth(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + if _, err := NewWithContext(context.Background(), testReference.Context().Registry, &badAuth{}, tprt, []string{testReference.Scope(PullScope)}); err == nil { + t.Errorf("NewWithContext() expected err, got nil") + } +} + +func TestTransportSelectionBearer(t *testing.T) { + request := 0 + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + request++ + switch request { + case 1: + // This is an https request that fails, causing us to fall back to http. + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + case 2: + w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + case 3: + hdr := r.Header.Get("Authorization") + if !strings.HasPrefix(hdr, "Basic ") { + t.Errorf("Header.Get(Authorization); got %v, want Basic prefix", hdr) + } + if got, want := r.FormValue("scope"), testReference.Scope(PullScope); got != want { + t.Errorf("FormValue(scope); got %v, want %v", got, want) + } + // Check that the service isn't set (we didn't specify it above) + // https://github.com/google/go-containerregistry/issues/1359 + if got, want := r.FormValue("service"), ""; got != want { + t.Errorf("FormValue(service); got %q, want %q", got, want) + } + w.Write([]byte(`{"token": "dfskdjhfkhsjdhfkjhsdf"}`)) + } + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) + if err != nil { + t.Errorf("NewWithContext() = %v", err) + } + if tpw, ok := tp.(*Wrapper); !ok { + t.Errorf("NewWithContext(); got %T, want *Wrapper", tp) + } else if _, ok := tpw.inner.(*bearerTransport); !ok { + t.Errorf("NewWithContext(); got %T, want *bearerTransport", tp) + } +} + +func TestTransportSelectionBearerMissingRealm(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Bearer service="gcr.io"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) + if err == nil || !strings.Contains(err.Error(), "missing realm") { + t.Errorf("NewWithContext() = %v, %v", tp, err) + } +} + +func TestTransportSelectionBearerAuthError(t *testing.T) { + request := 0 + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + request++ + switch request { + case 1: + w.Header().Set("WWW-Authenticate", `Bearer realm="http://foo.io"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + case 2: + http.Error(w, "Oops", http.StatusInternalServerError) + } + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) + if err == nil { + t.Errorf("NewWithContext() = %v", tp) + } +} + +func TestTransportSelectionUnrecognizedChallenge(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("WWW-Authenticate", `Unrecognized`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + })) + defer server.Close() + tprt := &http.Transport{ + Proxy: func(req *http.Request) (*url.URL, error) { + return url.Parse(server.URL) + }, + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + tp, err := NewWithContext(context.Background(), testReference.Context().Registry, basic, tprt, []string{testReference.Scope(PullScope)}) + if err == nil || !strings.Contains(err.Error(), "challenge") { + t.Errorf("NewWithContext() = %v, %v", tp, err) + } +} + +func TestTransportAlwaysTriesHttps(t *testing.T) { + // Use a NewTLSServer so that this speaks TLS even though it's localhost. + // This ensures that we try https even for local registries. + count := 0 + server := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + count++ + w.Write([]byte(`{"token": "dfskdjhfkhsjdhfkjhsdf"}`)) + })) + defer server.Close() + + u, err := url.Parse(server.URL) + if err != nil { + t.Errorf("Unexpected error during url.Parse: %v", err) + } + registry, err := name.NewRegistry(u.Host, name.WeakValidation) + if err != nil { + t.Errorf("Unexpected error during NewRegistry: %v", err) + } + + basic := &authn.Basic{Username: "foo", Password: "bar"} + tp, err := NewWithContext(context.Background(), registry, basic, server.Client().Transport, []string{testReference.Scope(PullScope)}) + if err != nil { + t.Fatalf("NewWithContext() = %v, %v", tp, err) + } + if count == 0 { + t.Errorf("failed to call TLS localhost server") + } +} diff --git a/pkg/v1/remote/transport/useragent.go b/pkg/v1/remote/transport/useragent.go new file mode 100644 index 0000000..74a9e71 --- /dev/null +++ b/pkg/v1/remote/transport/useragent.go @@ -0,0 +1,94 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package transport + +import ( + "fmt" + "net/http" + "runtime/debug" +) + +var ( + // Version can be set via: + // -ldflags="-X 'github.com/google/go-containerregistry/pkg/v1/remote/transport.Version=$TAG'" + Version string + + ggcrVersion = defaultUserAgent +) + +const ( + defaultUserAgent = "go-containerregistry" + moduleName = "github.com/google/go-containerregistry" +) + +type userAgentTransport struct { + inner http.RoundTripper + ua string +} + +func init() { + if v := version(); v != "" { + ggcrVersion = fmt.Sprintf("%s/%s", defaultUserAgent, v) + } +} + +func version() string { + if Version != "" { + // Version was set via ldflags, just return it. + return Version + } + + info, ok := debug.ReadBuildInfo() + if !ok { + return "" + } + + // Happens for crane and gcrane. + if info.Main.Path == moduleName { + return info.Main.Version + } + + // Anything else. + for _, dep := range info.Deps { + if dep.Path == moduleName { + return dep.Version + } + } + + return "" +} + +// NewUserAgent returns an http.Roundtripper that sets the user agent to +// The provided string plus additional go-containerregistry information, +// e.g. if provided "crane/v0.1.4" and this modules was built at v0.1.4: +// +// User-Agent: crane/v0.1.4 go-containerregistry/v0.1.4 +func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper { + if ua == "" { + ua = ggcrVersion + } else { + ua = fmt.Sprintf("%s %s", ua, ggcrVersion) + } + return &userAgentTransport{ + inner: inner, + ua: ua, + } +} + +// RoundTrip implements http.RoundTripper +func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) { + in.Header.Set("User-Agent", ut.ua) + return ut.inner.RoundTrip(in) +} diff --git a/pkg/v1/remote/write.go b/pkg/v1/remote/write.go new file mode 100644 index 0000000..5dbaa7c --- /dev/null +++ b/pkg/v1/remote/write.go @@ -0,0 +1,1003 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sort" + "strings" + + "github.com/google/go-containerregistry/internal/redact" + "github.com/google/go-containerregistry/internal/retry" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/types" + "golang.org/x/sync/errgroup" +) + +// Taggable is an interface that enables a manifest PUT (e.g. for tagging). +type Taggable interface { + RawManifest() ([]byte, error) +} + +// Write pushes the provided img to the specified image reference. +func Write(ref name.Reference, img v1.Image, options ...Option) (rerr error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return err + } + + var p *progress + if o.updates != nil { + p = &progress{updates: o.updates} + p.lastUpdate = &v1.Update{} + p.lastUpdate.Total, err = countImage(img, o.allowNondistributableArtifacts) + if err != nil { + return err + } + defer close(o.updates) + defer func() { _ = p.err(rerr) }() + } + return writeImage(o.context, ref, img, o, p) +} + +func writeImage(ctx context.Context, ref name.Reference, img v1.Image, o *options, progress *progress) error { + ls, err := img.Layers() + if err != nil { + return err + } + scopes := scopesForUploadingImage(ref.Context(), ls) + tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + w := writer{ + repo: ref.Context(), + client: &http.Client{Transport: tr}, + progress: progress, + backoff: o.retryBackoff, + predicate: o.retryPredicate, + } + + // Upload individual blobs and collect any errors. + blobChan := make(chan v1.Layer, 2*o.jobs) + g, gctx := errgroup.WithContext(ctx) + for i := 0; i < o.jobs; i++ { + // Start N workers consuming blobs to upload. + g.Go(func() error { + for b := range blobChan { + if err := w.uploadOne(gctx, b); err != nil { + return err + } + } + return nil + }) + } + + // Upload individual layers in goroutines and collect any errors. + // If we can dedupe by the layer digest, try to do so. If we can't determine + // the digest for whatever reason, we can't dedupe and might re-upload. + g.Go(func() error { + defer close(blobChan) + uploaded := map[v1.Hash]bool{} + for _, l := range ls { + l := l + + // Handle foreign layers. + mt, err := l.MediaType() + if err != nil { + return err + } + if !mt.IsDistributable() && !o.allowNondistributableArtifacts { + continue + } + + // Streaming layers calculate their digests while uploading them. Assume + // an error here indicates we need to upload the layer. + h, err := l.Digest() + if err == nil { + // If we can determine the layer's digest ahead of + // time, use it to dedupe uploads. + if uploaded[h] { + continue // Already uploading. + } + uploaded[h] = true + } + select { + case blobChan <- l: + case <-gctx.Done(): + return gctx.Err() + } + } + return nil + }) + + if l, err := partial.ConfigLayer(img); err != nil { + // We can't read the ConfigLayer, possibly because of streaming layers, + // since the layer DiffIDs haven't been calculated yet. Attempt to wait + // for the other layers to be uploaded, then try the config again. + if err := g.Wait(); err != nil { + return err + } + + // Now that all the layers are uploaded, try to upload the config file blob. + l, err := partial.ConfigLayer(img) + if err != nil { + return err + } + if err := w.uploadOne(ctx, l); err != nil { + return err + } + } else { + // We *can* read the ConfigLayer, so upload it concurrently with the layers. + g.Go(func() error { + return w.uploadOne(gctx, l) + }) + + // Wait for the layers + config. + if err := g.Wait(); err != nil { + return err + } + } + + // With all of the constituent elements uploaded, upload the manifest + // to commit the image. + return w.commitManifest(ctx, img, ref) +} + +// writer writes the elements of an image to a remote image reference. +type writer struct { + repo name.Repository + client *http.Client + + progress *progress + backoff Backoff + predicate retry.Predicate +} + +// url returns a url.Url for the specified path in the context of this remote image reference. +func (w *writer) url(path string) url.URL { + return url.URL{ + Scheme: w.repo.Registry.Scheme(), + Host: w.repo.RegistryStr(), + Path: path, + } +} + +// nextLocation extracts the fully-qualified URL to which we should send the next request in an upload sequence. +func (w *writer) nextLocation(resp *http.Response) (string, error) { + loc := resp.Header.Get("Location") + if len(loc) == 0 { + return "", errors.New("missing Location header") + } + u, err := url.Parse(loc) + if err != nil { + return "", err + } + + // If the location header returned is just a url path, then fully qualify it. + // We cannot simply call w.url, since there might be an embedded query string. + return resp.Request.URL.ResolveReference(u).String(), nil +} + +// checkExistingBlob checks if a blob exists already in the repository by making a +// HEAD request to the blob store API. GCR performs an existence check on the +// initiation if "mount" is specified, even if no "from" sources are specified. +// However, this is not broadly applicable to all registries, e.g. ECR. +func (w *writer) checkExistingBlob(ctx context.Context, h v1.Hash) (bool, error) { + u := w.url(fmt.Sprintf("/v2/%s/blobs/%s", w.repo.RepositoryStr(), h.String())) + + req, err := http.NewRequest(http.MethodHead, u.String(), nil) + if err != nil { + return false, err + } + + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + +// checkExistingManifest checks if a manifest exists already in the repository +// by making a HEAD request to the manifest API. +func (w *writer) checkExistingManifest(ctx context.Context, h v1.Hash, mt types.MediaType) (bool, error) { + u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), h.String())) + + req, err := http.NewRequest(http.MethodHead, u.String(), nil) + if err != nil { + return false, err + } + req.Header.Set("Accept", string(mt)) + + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return false, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil { + return false, err + } + + return resp.StatusCode == http.StatusOK, nil +} + +// initiateUpload initiates the blob upload, which starts with a POST that can +// optionally include the hash of the layer and a list of repositories from +// which that layer might be read. On failure, an error is returned. +// On success, the layer was either mounted (nothing more to do) or a blob +// upload was initiated and the body of that blob should be sent to the returned +// location. +func (w *writer) initiateUpload(ctx context.Context, from, mount, origin string) (location string, mounted bool, err error) { + u := w.url(fmt.Sprintf("/v2/%s/blobs/uploads/", w.repo.RepositoryStr())) + uv := url.Values{} + if mount != "" && from != "" { + // Quay will fail if we specify a "mount" without a "from". + uv.Set("mount", mount) + uv.Set("from", from) + if origin != "" { + uv.Set("origin", origin) + } + } + u.RawQuery = uv.Encode() + + // Make the request to initiate the blob upload. + req, err := http.NewRequest(http.MethodPost, u.String(), nil) + if err != nil { + return "", false, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return "", false, err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusCreated, http.StatusAccepted); err != nil { + if origin != "" && origin != w.repo.RegistryStr() { + // https://github.com/google/go-containerregistry/issues/1404 + logs.Warn.Printf("retrying without mount: %v", err) + return w.initiateUpload(ctx, "", "", "") + } + return "", false, err + } + + // Check the response code to determine the result. + switch resp.StatusCode { + case http.StatusCreated: + // We're done, we were able to fast-path. + return "", true, nil + case http.StatusAccepted: + // Proceed to PATCH, upload has begun. + loc, err := w.nextLocation(resp) + return loc, false, err + default: + panic("Unreachable: initiateUpload") + } +} + +// streamBlob streams the contents of the blob to the specified location. +// On failure, this will return an error. On success, this will return the location +// header indicating how to commit the streamed blob. +func (w *writer) streamBlob(ctx context.Context, layer v1.Layer, streamLocation string) (commitLocation string, rerr error) { + reset := func() {} + defer func() { + if rerr != nil { + reset() + } + }() + blob, err := layer.Compressed() + if err != nil { + return "", err + } + + getBody := layer.Compressed + if w.progress != nil { + var count int64 + blob = &progressReader{rc: blob, progress: w.progress, count: &count} + getBody = func() (io.ReadCloser, error) { + blob, err := layer.Compressed() + if err != nil { + return nil, err + } + return &progressReader{rc: blob, progress: w.progress, count: &count}, nil + } + reset = func() { + w.progress.complete(-count) + } + } + + req, err := http.NewRequest(http.MethodPatch, streamLocation, blob) + if err != nil { + return "", err + } + if _, ok := layer.(*stream.Layer); !ok { + // We can't retry streaming layers. + req.GetBody = getBody + } + req.Header.Set("Content-Type", "application/octet-stream") + + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusNoContent, http.StatusAccepted, http.StatusCreated); err != nil { + return "", err + } + + // The blob has been uploaded, return the location header indicating + // how to commit this layer. + return w.nextLocation(resp) +} + +// commitBlob commits this blob by sending a PUT to the location returned from +// streaming the blob. +func (w *writer) commitBlob(ctx context.Context, location, digest string) error { + u, err := url.Parse(location) + if err != nil { + return err + } + v := u.Query() + v.Set("digest", digest) + u.RawQuery = v.Encode() + + req, err := http.NewRequest(http.MethodPut, u.String(), nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/octet-stream") + + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + return transport.CheckError(resp, http.StatusCreated) +} + +// incrProgress increments and sends a progress update, if WithProgress is used. +func (w *writer) incrProgress(written int64) { + if w.progress == nil { + return + } + w.progress.complete(written) +} + +// uploadOne performs a complete upload of a single layer. +func (w *writer) uploadOne(ctx context.Context, l v1.Layer) error { + tryUpload := func() error { + ctx := retry.Never(ctx) + var from, mount, origin string + if h, err := l.Digest(); err == nil { + // If we know the digest, this isn't a streaming layer. Do an existence + // check so we can skip uploading the layer if possible. + existing, err := w.checkExistingBlob(ctx, h) + if err != nil { + return err + } + if existing { + size, err := l.Size() + if err != nil { + return err + } + w.incrProgress(size) + logs.Progress.Printf("existing blob: %v", h) + return nil + } + + mount = h.String() + } + if ml, ok := l.(*MountableLayer); ok { + from = ml.Reference.Context().RepositoryStr() + origin = ml.Reference.Context().RegistryStr() + } + + location, mounted, err := w.initiateUpload(ctx, from, mount, origin) + if err != nil { + return err + } else if mounted { + size, err := l.Size() + if err != nil { + return err + } + w.incrProgress(size) + h, err := l.Digest() + if err != nil { + return err + } + logs.Progress.Printf("mounted blob: %s", h.String()) + return nil + } + + // Only log layers with +json or +yaml. We can let through other stuff if it becomes popular. + // TODO(opencontainers/image-spec#791): Would be great to have an actual parser. + mt, err := l.MediaType() + if err != nil { + return err + } + smt := string(mt) + if !(strings.HasSuffix(smt, "+json") || strings.HasSuffix(smt, "+yaml")) { + ctx = redact.NewContext(ctx, "omitting binary blobs from logs") + } + + location, err = w.streamBlob(ctx, l, location) + if err != nil { + return err + } + + h, err := l.Digest() + if err != nil { + return err + } + digest := h.String() + + if err := w.commitBlob(ctx, location, digest); err != nil { + return err + } + logs.Progress.Printf("pushed blob: %s", digest) + return nil + } + + return retry.Retry(tryUpload, w.predicate, w.backoff) +} + +type withLayer interface { + Layer(v1.Hash) (v1.Layer, error) +} + +func (w *writer) writeIndex(ctx context.Context, ref name.Reference, ii v1.ImageIndex, options ...Option) error { + index, err := ii.IndexManifest() + if err != nil { + return err + } + + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return err + } + + // TODO(#803): Pipe through remote.WithJobs and upload these in parallel. + for _, desc := range index.Manifests { + ref := ref.Context().Digest(desc.Digest.String()) + exists, err := w.checkExistingManifest(ctx, desc.Digest, desc.MediaType) + if err != nil { + return err + } + if exists { + logs.Progress.Print("existing manifest: ", desc.Digest) + continue + } + + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + ii, err := ii.ImageIndex(desc.Digest) + if err != nil { + return err + } + if err := w.writeIndex(ctx, ref, ii, options...); err != nil { + return err + } + case types.OCIManifestSchema1, types.DockerManifestSchema2: + img, err := ii.Image(desc.Digest) + if err != nil { + return err + } + if err := writeImage(ctx, ref, img, o, w.progress); err != nil { + return err + } + default: + // Workaround for #819. + if wl, ok := ii.(withLayer); ok { + layer, err := wl.Layer(desc.Digest) + if err != nil { + return err + } + if err := w.uploadOne(ctx, layer); err != nil { + return err + } + } + } + } + + // With all of the constituent elements uploaded, upload the manifest + // to commit the image. + return w.commitManifest(ctx, ii, ref) +} + +type withMediaType interface { + MediaType() (types.MediaType, error) +} + +// This is really silly, but go interfaces don't let me satisfy remote.Taggable +// with remote.Descriptor because of name collisions between method names and +// struct fields. +// +// Use reflection to either pull the v1.Descriptor out of remote.Descriptor or +// create a descriptor based on the RawManifest and (optionally) MediaType. +func unpackTaggable(t Taggable) ([]byte, *v1.Descriptor, error) { + if d, ok := t.(*Descriptor); ok { + return d.Manifest, &d.Descriptor, nil + } + b, err := t.RawManifest() + if err != nil { + return nil, nil, err + } + + // A reasonable default if Taggable doesn't implement MediaType. + mt := types.DockerManifestSchema2 + + if wmt, ok := t.(withMediaType); ok { + m, err := wmt.MediaType() + if err != nil { + return nil, nil, err + } + mt = m + } + + h, sz, err := v1.SHA256(bytes.NewReader(b)) + if err != nil { + return nil, nil, err + } + + return b, &v1.Descriptor{ + MediaType: mt, + Size: sz, + Digest: h, + }, nil +} + +// commitSubjectReferrers is responsible for updating the fallback tag manifest to track descriptors referring to a subject for registries that don't yet support the Referrers API. +// TODO: use conditional requests to avoid race conditions +func (w *writer) commitSubjectReferrers(ctx context.Context, sub name.Digest, add v1.Descriptor) error { + // Check if the registry supports Referrers API. + // TODO: This should be done once per registry, not once per subject. + u := w.url(fmt.Sprintf("/v2/%s/referrers/%s", w.repo.RepositoryStr(), sub.DigestStr())) + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return err + } + req.Header.Set("Accept", string(types.OCIImageIndex)) + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound, http.StatusBadRequest); err != nil { + return err + } + if resp.StatusCode == http.StatusOK { + // The registry supports Referrers API. The registry is responsible for updating the referrers list. + return nil + } + + // The registry doesn't support Referrers API, we need to update the manifest tagged with the fallback tag. + // Make the request to GET the current manifest. + t := fallbackTag(sub) + u = w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), t.Identifier())) + req, err = http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return err + } + req.Header.Set("Accept", string(types.OCIImageIndex)) + resp, err = w.client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + var im v1.IndexManifest + if err := transport.CheckError(resp, http.StatusOK, http.StatusNotFound); err != nil { + return err + } else if resp.StatusCode == http.StatusNotFound { + // Not found just means there are no attachments. Start with an empty index. + im = v1.IndexManifest{ + SchemaVersion: 2, + MediaType: types.OCIImageIndex, + Manifests: []v1.Descriptor{add}, + } + } else { + if err := json.NewDecoder(resp.Body).Decode(&im); err != nil { + return err + } + if im.SchemaVersion != 2 { + return fmt.Errorf("fallback tag manifest is not a schema version 2: %d", im.SchemaVersion) + } + if im.MediaType != types.OCIImageIndex { + return fmt.Errorf("fallback tag manifest is not an OCI image index: %s", im.MediaType) + } + for _, desc := range im.Manifests { + if desc.Digest == add.Digest { + // The digest is already attached, nothing to do. + logs.Progress.Printf("fallback tag %s already had referrer", t.Identifier()) + return nil + } + } + // Append the new descriptor to the index. + im.Manifests = append(im.Manifests, add) + } + + // Sort the manifests for reproducibility. + sort.Slice(im.Manifests, func(i, j int) bool { + return im.Manifests[i].Digest.String() < im.Manifests[j].Digest.String() + }) + logs.Progress.Printf("updating fallback tag %s with new referrer", t.Identifier()) + if err := w.commitManifest(ctx, fallbackTaggable{im}, t); err != nil { + return err + } + return nil +} + +type fallbackTaggable struct { + im v1.IndexManifest +} + +func (f fallbackTaggable) RawManifest() ([]byte, error) { return json.Marshal(f.im) } +func (f fallbackTaggable) MediaType() (types.MediaType, error) { return types.OCIImageIndex, nil } + +// commitManifest does a PUT of the image's manifest. +func (w *writer) commitManifest(ctx context.Context, t Taggable, ref name.Reference) error { + // If the manifest refers to a subject, we need to check whether we need to update the fallback tag manifest. + raw, err := t.RawManifest() + if err != nil { + return err + } + var mf struct { + MediaType types.MediaType `json:"mediaType"` + Subject *v1.Descriptor `json:"subject,omitempty"` + Config struct { + MediaType types.MediaType `json:"mediaType"` + } `json:"config"` + } + if err := json.Unmarshal(raw, &mf); err != nil { + return err + } + + tryUpload := func() error { + ctx := retry.Never(ctx) + raw, desc, err := unpackTaggable(t) + if err != nil { + return err + } + + u := w.url(fmt.Sprintf("/v2/%s/manifests/%s", w.repo.RepositoryStr(), ref.Identifier())) + + // Make the request to PUT the serialized manifest + req, err := http.NewRequest(http.MethodPut, u.String(), bytes.NewBuffer(raw)) + if err != nil { + return err + } + req.Header.Set("Content-Type", string(desc.MediaType)) + + resp, err := w.client.Do(req.WithContext(ctx)) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := transport.CheckError(resp, http.StatusOK, http.StatusCreated, http.StatusAccepted); err != nil { + return err + } + + // If the manifest referred to a subject, we may need to update the fallback tag manifest. + // TODO: If this fails, we'll retry the whole upload. We should retry just this part. + if mf.Subject != nil { + h, size, err := v1.SHA256(bytes.NewReader(raw)) + if err != nil { + return err + } + desc := v1.Descriptor{ + ArtifactType: string(mf.Config.MediaType), + MediaType: mf.MediaType, + Digest: h, + Size: size, + } + if err := w.commitSubjectReferrers(ctx, + ref.Context().Digest(mf.Subject.Digest.String()), + desc); err != nil { + return err + } + } + + // The image was successfully pushed! + logs.Progress.Printf("%v: digest: %v size: %d", ref, desc.Digest, desc.Size) + w.incrProgress(int64(len(raw))) + return nil + } + + return retry.Retry(tryUpload, w.predicate, w.backoff) +} + +func scopesForUploadingImage(repo name.Repository, layers []v1.Layer) []string { + // use a map as set to remove duplicates scope strings + scopeSet := map[string]struct{}{} + + for _, l := range layers { + if ml, ok := l.(*MountableLayer); ok { + // we will add push scope for ref.Context() after the loop. + // for now we ask pull scope for references of the same registry + if ml.Reference.Context().String() != repo.String() && ml.Reference.Context().Registry.String() == repo.Registry.String() { + scopeSet[ml.Reference.Scope(transport.PullScope)] = struct{}{} + } + } + } + + scopes := make([]string, 0) + // Push scope should be the first element because a few registries just look at the first scope to determine access. + scopes = append(scopes, repo.Scope(transport.PushScope)) + + for scope := range scopeSet { + scopes = append(scopes, scope) + } + + return scopes +} + +// WriteIndex pushes the provided ImageIndex to the specified image reference. +// WriteIndex will attempt to push all of the referenced manifests before +// attempting to push the ImageIndex, to retain referential integrity. +func WriteIndex(ref name.Reference, ii v1.ImageIndex, options ...Option) (rerr error) { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return err + } + + scopes := []string{ref.Scope(transport.PushScope)} + tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + w := writer{ + repo: ref.Context(), + client: &http.Client{Transport: tr}, + backoff: o.retryBackoff, + predicate: o.retryPredicate, + } + + if o.updates != nil { + w.progress = &progress{updates: o.updates} + w.progress.lastUpdate = &v1.Update{} + + defer close(o.updates) + defer func() { w.progress.err(rerr) }() + + w.progress.lastUpdate.Total, err = countIndex(ii, o.allowNondistributableArtifacts) + if err != nil { + return err + } + } + + return w.writeIndex(o.context, ref, ii, options...) +} + +// countImage counts the total size of all layers + config blob + manifest for +// an image. It de-dupes duplicate layers. +func countImage(img v1.Image, allowNondistributableArtifacts bool) (int64, error) { + var total int64 + ls, err := img.Layers() + if err != nil { + return 0, err + } + seen := map[v1.Hash]bool{} + for _, l := range ls { + // Handle foreign layers. + mt, err := l.MediaType() + if err != nil { + return 0, err + } + if !mt.IsDistributable() && !allowNondistributableArtifacts { + continue + } + + // TODO: support streaming layers which update the total count as they write. + if _, ok := l.(*stream.Layer); ok { + return 0, errors.New("cannot use stream.Layer and WithProgress") + } + + // Dedupe layers. + d, err := l.Digest() + if err != nil { + return 0, err + } + if seen[d] { + continue + } + seen[d] = true + + size, err := l.Size() + if err != nil { + return 0, err + } + total += size + } + b, err := img.RawConfigFile() + if err != nil { + return 0, err + } + total += int64(len(b)) + size, err := img.Size() + if err != nil { + return 0, err + } + total += size + return total, nil +} + +// countIndex counts the total size of all images + sub-indexes for an index. +// It does not attempt to de-dupe duplicate images, etc. +func countIndex(idx v1.ImageIndex, allowNondistributableArtifacts bool) (int64, error) { + var total int64 + mf, err := idx.IndexManifest() + if err != nil { + return 0, err + } + + for _, desc := range mf.Manifests { + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + sidx, err := idx.ImageIndex(desc.Digest) + if err != nil { + return 0, err + } + size, err := countIndex(sidx, allowNondistributableArtifacts) + if err != nil { + return 0, err + } + total += size + case types.OCIManifestSchema1, types.DockerManifestSchema2: + simg, err := idx.Image(desc.Digest) + if err != nil { + return 0, err + } + size, err := countImage(simg, allowNondistributableArtifacts) + if err != nil { + return 0, err + } + total += size + default: + // Workaround for #819. + if wl, ok := idx.(withLayer); ok { + layer, err := wl.Layer(desc.Digest) + if err != nil { + return 0, err + } + size, err := layer.Size() + if err != nil { + return 0, err + } + total += size + } + } + } + + size, err := idx.Size() + if err != nil { + return 0, err + } + total += size + return total, nil +} + +// WriteLayer uploads the provided Layer to the specified repo. +func WriteLayer(repo name.Repository, layer v1.Layer, options ...Option) (rerr error) { + o, err := makeOptions(repo, options...) + if err != nil { + return err + } + scopes := scopesForUploadingImage(repo, []v1.Layer{layer}) + tr, err := transport.NewWithContext(o.context, repo.Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + w := writer{ + repo: repo, + client: &http.Client{Transport: tr}, + backoff: o.retryBackoff, + predicate: o.retryPredicate, + } + + if o.updates != nil { + w.progress = &progress{updates: o.updates} + w.progress.lastUpdate = &v1.Update{} + + defer close(o.updates) + defer func() { w.progress.err(rerr) }() + + // TODO: support streaming layers which update the total count as they write. + if _, ok := layer.(*stream.Layer); ok { + return errors.New("cannot use stream.Layer and WithProgress") + } + size, err := layer.Size() + if err != nil { + return err + } + w.progress.total(size) + } + return w.uploadOne(o.context, layer) +} + +// Tag adds a tag to the given Taggable via PUT /v2/.../manifests/<tag> +// +// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and +// remote.Descriptor. +// +// If t implements MediaType, we will use that for the Content-Type, otherwise +// we will default to types.DockerManifestSchema2. +// +// Tag does not attempt to write anything other than the manifest, so callers +// should ensure that all blobs or manifests that are referenced by t exist +// in the target registry. +func Tag(tag name.Tag, t Taggable, options ...Option) error { + return Put(tag, t, options...) +} + +// Put adds a manifest from the given Taggable via PUT /v1/.../manifest/<ref> +// +// Notable implementations of Taggable are v1.Image, v1.ImageIndex, and +// remote.Descriptor. +// +// If t implements MediaType, we will use that for the Content-Type, otherwise +// we will default to types.DockerManifestSchema2. +// +// Put does not attempt to write anything other than the manifest, so callers +// should ensure that all blobs or manifests that are referenced by t exist +// in the target registry. +func Put(ref name.Reference, t Taggable, options ...Option) error { + o, err := makeOptions(ref.Context(), options...) + if err != nil { + return err + } + scopes := []string{ref.Scope(transport.PushScope)} + + // TODO: This *always* does a token exchange. For some registries, + // that's pretty slow. Some ideas; + // * Tag could take a list of tags. + // * Allow callers to pass in a transport.Transport, typecheck + // it to allow them to reuse the transport across multiple calls. + // * WithTag option to do multiple manifest PUTs in commitManifest. + tr, err := transport.NewWithContext(o.context, ref.Context().Registry, o.auth, o.transport, scopes) + if err != nil { + return err + } + w := writer{ + repo: ref.Context(), + client: &http.Client{Transport: tr}, + backoff: o.retryBackoff, + predicate: o.retryPredicate, + } + + return w.commitManifest(o.context, t, ref) +} diff --git a/pkg/v1/remote/write_test.go b/pkg/v1/remote/write_test.go new file mode 100644 index 0000000..7235c96 --- /dev/null +++ b/pkg/v1/remote/write_test.go @@ -0,0 +1,1643 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package remote + +import ( + "bytes" + "context" + "crypto" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "sync/atomic" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/google/go-containerregistry/pkg/v1/stream" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func mustNewTag(t *testing.T, s string) name.Tag { + tag, err := name.NewTag(s, name.WeakValidation) + if err != nil { + t.Fatalf("NewTag(%v) = %v", s, err) + } + return tag +} + +func TestUrl(t *testing.T) { + tests := []struct { + tag string + path string + url string + }{{ + tag: "gcr.io/foo/bar:latest", + path: "/v2/foo/bar/manifests/latest", + url: "https://gcr.io/v2/foo/bar/manifests/latest", + }, { + tag: "localhost:8080/foo/bar:baz", + path: "/v2/foo/bar/blobs/upload", + url: "http://localhost:8080/v2/foo/bar/blobs/upload", + }} + + for _, test := range tests { + w := &writer{ + repo: mustNewTag(t, test.tag).Context(), + } + if got, want := w.url(test.path), test.url; got.String() != want { + t.Errorf("url(%v) = %v, want %v", test.path, got.String(), want) + } + } +} + +func TestNextLocation(t *testing.T) { + tests := []struct { + location string + url string + }{{ + location: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah", + url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah", + }, { + location: "/v2/foo/bar/blobs/uploads/1234567?baz=blah", + url: "https://gcr.io/v2/foo/bar/blobs/uploads/1234567?baz=blah", + }} + + ref := mustNewTag(t, "gcr.io/foo/bar:latest") + w := &writer{ + repo: ref.Context(), + } + + for _, test := range tests { + resp := &http.Response{ + Header: map[string][]string{ + "Location": {test.location}, + }, + Request: &http.Request{ + URL: &url.URL{ + Scheme: ref.Registry.Scheme(), + Host: ref.RegistryStr(), + }, + }, + } + + got, err := w.nextLocation(resp) + if err != nil { + t.Errorf("nextLocation(%v) = %v", resp, err) + } + want := test.url + if got != want { + t.Errorf("nextLocation(%v) = %v, want %v", resp, got, want) + } + } +} + +type closer interface { + Close() +} + +func setupImage(t *testing.T) v1.Image { + rnd, err := random.Image(1024, 1) + if err != nil { + t.Fatalf("random.Image() = %v", err) + } + return rnd +} + +func setupIndex(t *testing.T, children int64) v1.ImageIndex { + rnd, err := random.Index(1024, 1, children) + if err != nil { + t.Fatalf("random.Index() = %v", err) + } + return rnd +} + +func mustConfigName(t *testing.T, img v1.Image) v1.Hash { + h, err := img.ConfigName() + if err != nil { + t.Fatalf("ConfigName() = %v", err) + } + return h +} + +func setupWriter(repo string, handler http.HandlerFunc) (*writer, closer, error) { + server := httptest.NewServer(handler) + return setupWriterWithServer(server, repo) +} + +func setupWriterWithServer(server *httptest.Server, repo string) (*writer, closer, error) { + u, err := url.Parse(server.URL) + if err != nil { + server.Close() + return nil, nil, err + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, repo), name.WeakValidation) + if err != nil { + server.Close() + return nil, nil, err + } + + return &writer{ + repo: tag.Context(), + client: http.DefaultClient, + predicate: defaultRetryPredicate, + backoff: defaultRetryBackoff, + }, server, nil +} + +func TestCheckExistingBlob(t *testing.T) { + tests := []struct { + name string + status int + existing bool + wantErr bool + }{{ + name: "success", + status: http.StatusOK, + existing: true, + }, { + name: "not found", + status: http.StatusNotFound, + existing: false, + }, { + name: "error", + status: http.StatusInternalServerError, + existing: false, + wantErr: true, + }} + + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "foo/bar" + expectedPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String()) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + http.Error(w, http.StatusText(test.status), test.status) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + existing, err := w.checkExistingBlob(context.Background(), h) + if test.existing != existing { + t.Errorf("checkExistingBlob() = %v, want %v", existing, test.existing) + } + if err != nil && !test.wantErr { + t.Errorf("checkExistingBlob() = %v", err) + } else if err == nil && test.wantErr { + t.Error("checkExistingBlob() wanted err, got nil") + } + }) + } +} + +func TestInitiateUploadNoMountsExists(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "foo/bar" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{"baz/bar"}, + }.Encode() + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Mounted", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if !mounted { + t.Error("initiateUpload() = !mounted, want mounted") + } +} + +func TestInitiateUploadNoMountsInitiated(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "baz/blah" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{"baz/bar"}, + }.Encode() + expectedLocation := "https://somewhere.io/upload?foo=bar" + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + w.Header().Set("Location", expectedLocation) + http.Error(w, "Initiated", http.StatusAccepted) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if mounted { + t.Error("initiateUpload() = mounted, want !mounted") + } + if location != expectedLocation { + t.Errorf("initiateUpload(); got %v, want %v", location, expectedLocation) + } +} + +func TestInitiateUploadNoMountsBadStatus(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "ugh/another" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{"baz/bar"}, + }.Encode() + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Unknown", http.StatusNoContent) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + location, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "") + if err == nil { + t.Errorf("intiateUpload() = %v, %v; wanted error", location, mounted) + } +} + +func TestInitiateUploadMountsWithMountFromDifferentRegistry(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "yet/again" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{"baz/bar"}, + }.Encode() + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Mounted", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + _, mounted, err := w.initiateUpload(context.Background(), "baz/bar", h.String(), "") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if !mounted { + t.Error("initiateUpload() = !mounted, want mounted") + } +} + +func TestInitiateUploadMountsWithMountFromTheSameRegistry(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedMountRepo := "a/different/repo" + expectedRepo := "yet/again" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{expectedMountRepo}, + }.Encode() + + serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Mounted", http.StatusCreated) + }) + server := httptest.NewServer(serverHandler) + + w, closer, err := setupWriterWithServer(server, expectedRepo) + if err != nil { + t.Fatalf("setupWriterWithServer() = %v", err) + } + defer closer.Close() + + _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if !mounted { + t.Error("initiateUpload() = !mounted, want mounted") + } +} + +func TestInitiateUploadMountsWithOrigin(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedMountRepo := "a/different/repo" + expectedRepo := "yet/again" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedOrigin := "fakeOrigin" + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{expectedMountRepo}, + "origin": []string{expectedOrigin}, + }.Encode() + + serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Mounted", http.StatusCreated) + }) + server := httptest.NewServer(serverHandler) + + w, closer, err := setupWriterWithServer(server, expectedRepo) + if err != nil { + t.Fatalf("setupWriterWithServer() = %v", err) + } + defer closer.Close() + + _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if !mounted { + t.Error("initiateUpload() = !mounted, want mounted") + } +} + +func TestInitiateUploadMountsWithOriginFallback(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedMountRepo := "a/different/repo" + expectedRepo := "yet/again" + expectedPath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + expectedOrigin := "fakeOrigin" + expectedQuery := url.Values{ + "mount": []string{h.String()}, + "from": []string{expectedMountRepo}, + "origin": []string{expectedOrigin}, + }.Encode() + + queries := []string{expectedQuery, ""} + queryCount := 0 + + serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != queries[queryCount] { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + if queryCount == 0 { + http.Error(w, "nope", http.StatusUnauthorized) + } else { + http.Error(w, "Mounted", http.StatusCreated) + } + queryCount++ + }) + server := httptest.NewServer(serverHandler) + + w, closer, err := setupWriterWithServer(server, expectedRepo) + if err != nil { + t.Fatalf("setupWriterWithServer() = %v", err) + } + defer closer.Close() + + _, mounted, err := w.initiateUpload(context.Background(), expectedMountRepo, h.String(), "fakeOrigin") + if err != nil { + t.Errorf("intiateUpload() = %v", err) + } + if !mounted { + t.Error("initiateUpload() = !mounted, want mounted") + } +} + +func TestDedupeLayers(t *testing.T) { + newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, 10000))) } + + img, err := random.Image(1024, 3) + if err != nil { + t.Fatalf("random.Image: %v", err) + } + + // Append three identical tarball.Layers, which should be deduped + // because contents can be hashed before uploading. + for i := 0; i < 3; i++ { + tl, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return newBlob(), nil }) + if err != nil { + t.Fatalf("LayerFromOpener(#%d): %v", i, err) + } + img, err = mutate.AppendLayers(img, tl) + if err != nil { + t.Fatalf("mutate.AppendLayer(#%d): %v", i, err) + } + } + + // Append three identical stream.Layers, whose uploads will *not* be + // deduped since Write can't tell they're identical ahead of time. + for i := 0; i < 3; i++ { + sl := stream.NewLayer(newBlob()) + img, err = mutate.AppendLayers(img, sl) + if err != nil { + t.Fatalf("mutate.AppendLayer(#%d): %v", i, err) + } + } + + expectedRepo := "write/time" + headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + uploadPath := "/upload" + commitPath := "/commit" + var numUploads int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath { + http.Error(w, "NotFound", http.StatusNotFound) + return + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + w.Header().Set("Location", uploadPath) + http.Error(w, "Accepted", http.StatusAccepted) + case uploadPath: + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + atomic.AddInt32(&numUploads, 1) + w.Header().Set("Location", commitPath) + http.Error(w, "Created", http.StatusCreated) + case commitPath: + http.Error(w, "Created", http.StatusCreated) + case manifestPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := Write(tag, img); err != nil { + t.Errorf("Write: %v", err) + } + + // 3 random layers, 1 tarball layer (deduped), 3 stream layers (not deduped), 1 image config blob + wantUploads := int32(3 + 1 + 3 + 1) + if numUploads != wantUploads { + t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads) + } +} + +func TestStreamBlob(t *testing.T) { + img := setupImage(t) + expectedPath := "/vWhatever/I/decide" + expectedCommitLocation := "https://commit.io/v12/blob" + + w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + got, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("ReadAll(Body) = %v", err) + } + want, err := img.RawConfigFile() + if err != nil { + t.Errorf("RawConfigFile() = %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("bytes.Equal(); got %v, want %v", got, want) + } + w.Header().Set("Location", expectedCommitLocation) + http.Error(w, "Created", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + streamLocation := w.url(expectedPath) + + l, err := partial.ConfigLayer(img) + if err != nil { + t.Fatalf("ConfigLayer: %v", err) + } + + commitLocation, err := w.streamBlob(context.Background(), l, streamLocation.String()) + if err != nil { + t.Errorf("streamBlob() = %v", err) + } + if commitLocation != expectedCommitLocation { + t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation) + } +} + +func TestStreamLayer(t *testing.T) { + var n, wantSize int64 = 10000, 49 + newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) } + wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e" + + expectedPath := "/vWhatever/I/decide" + expectedCommitLocation := "https://commit.io/v12/blob" + w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + + h := crypto.SHA256.New() + s, err := io.Copy(h, r.Body) + if err != nil { + t.Errorf("Reading body: %v", err) + } + if s != wantSize { + t.Errorf("Received %d bytes, want %d", s, wantSize) + } + gotDigest := "sha256:" + hex.EncodeToString(h.Sum(nil)) + if gotDigest != wantDigest { + t.Errorf("Received bytes with digest %q, want %q", gotDigest, wantDigest) + } + + w.Header().Set("Location", expectedCommitLocation) + http.Error(w, "Created", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + streamLocation := w.url(expectedPath) + sl := stream.NewLayer(newBlob()) + + commitLocation, err := w.streamBlob(context.Background(), sl, streamLocation.String()) + if err != nil { + t.Errorf("streamBlob: %v", err) + } + if commitLocation != expectedCommitLocation { + t.Errorf("streamBlob(); got %v, want %v", commitLocation, expectedCommitLocation) + } +} + +func TestCommitBlob(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedPath := "/no/commitment/issues" + expectedQuery := url.Values{ + "digest": []string{h.String()}, + }.Encode() + + w, closer, err := setupWriter("what/ever", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if r.URL.RawQuery != expectedQuery { + t.Errorf("RawQuery; got %v, want %v", r.URL.RawQuery, expectedQuery) + } + http.Error(w, "Created", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + commitLocation := w.url(expectedPath) + + if err := w.commitBlob(context.Background(), commitLocation.String(), h.String()); err != nil { + t.Errorf("commitBlob() = %v", err) + } +} + +func TestUploadOne(t *testing.T) { + img := setupImage(t) + h := mustConfigName(t, img) + expectedRepo := "baz/blah" + headPath := fmt.Sprintf("/v2/%s/blobs/%s", expectedRepo, h.String()) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + streamPath := "/path/to/upload" + commitPath := "/path/to/commit" + ctx := context.Background() + + uploaded := false + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case headPath: + if r.Method != http.MethodHead { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) + } + if uploaded { + return + } + http.Error(w, "NotFound", http.StatusNotFound) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + w.Header().Set("Location", streamPath) + http.Error(w, "Initiated", http.StatusAccepted) + case streamPath: + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + got, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("ReadAll(Body) = %v", err) + } + want, err := img.RawConfigFile() + if err != nil { + t.Errorf("RawConfigFile() = %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("bytes.Equal(); got %v, want %v", got, want) + } + w.Header().Set("Location", commitPath) + http.Error(w, "Initiated", http.StatusAccepted) + case commitPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + uploaded = true + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + l, err := partial.ConfigLayer(img) + if err != nil { + t.Fatalf("ConfigLayer: %v", err) + } + ml := &MountableLayer{ + Layer: l, + Reference: w.repo.Digest(h.String()), + } + if err := w.uploadOne(ctx, ml); err != nil { + t.Errorf("uploadOne() = %v", err) + } + // Hit the existing blob path. + if err := w.uploadOne(ctx, l); err != nil { + t.Errorf("uploadOne() = %v", err) + } +} + +func TestUploadOneStreamedLayer(t *testing.T) { + expectedRepo := "baz/blah" + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + streamPath := "/path/to/upload" + commitPath := "/path/to/commit" + ctx := context.Background() + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + w.Header().Set("Location", streamPath) + http.Error(w, "Initiated", http.StatusAccepted) + case streamPath: + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + // TODO(jasonhall): What should we check here? + w.Header().Set("Location", commitPath) + http.Error(w, "Initiated", http.StatusAccepted) + case commitPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + var n, wantSize int64 = 10000, 49 + newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) } + wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e" + wantDiffID := "sha256:27dd1f61b867b6a0f6e9d8a41c43231de52107e53ae424de8f847b821db4b711" + l := stream.NewLayer(newBlob()) + if err := w.uploadOne(ctx, l); err != nil { + t.Fatalf("uploadOne: %v", err) + } + + if dig, err := l.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if dig.String() != wantDigest { + t.Errorf("Digest got %q, want %q", dig, wantDigest) + } + if diffID, err := l.DiffID(); err != nil { + t.Errorf("DiffID: %v", err) + } else if diffID.String() != wantDiffID { + t.Errorf("DiffID got %q, want %q", diffID, wantDiffID) + } + if size, err := l.Size(); err != nil { + t.Errorf("Size: %v", err) + } else if size != wantSize { + t.Errorf("Size got %d, want %d", size, wantSize) + } +} + +func TestCommitImage(t *testing.T) { + img := setupImage(t) + ctx := context.Background() + + expectedRepo := "foo/bar" + expectedPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + got, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("ReadAll(Body) = %v", err) + } + want, err := img.RawManifest() + if err != nil { + t.Errorf("RawManifest() = %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("bytes.Equal(); got %v, want %v", got, want) + } + mt, err := img.MediaType() + if err != nil { + t.Errorf("MediaType() = %v", err) + } + if got, want := r.Header.Get("Content-Type"), string(mt); got != want { + t.Errorf("Header; got %v, want %v", got, want) + } + http.Error(w, "Created", http.StatusCreated) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + if err := w.commitManifest(ctx, img, w.repo.Tag("latest")); err != nil { + t.Error("commitManifest() = ", err) + } +} + +func TestWrite(t *testing.T) { + img := setupImage(t) + expectedRepo := "write/time" + headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath { + http.Error(w, "NotFound", http.StatusNotFound) + return + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + http.Error(w, "Mounted", http.StatusCreated) + case manifestPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := Write(tag, img); err != nil { + t.Errorf("Write() = %v", err) + } +} + +func TestWriteWithErrors(t *testing.T) { + img := setupImage(t) + expectedRepo := "write/time" + headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + + errorBody := `{"errors":[{"code":"NAME_INVALID","message":"some explanation of how things were messed up."}],"StatusCode":400}` + expectedErrMsg, err := regexp.Compile(`POST .+ NAME_INVALID: some explanation of how things were messed up.`) + if err != nil { + t.Error(err) + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath { + http.Error(w, "NotFound", http.StatusNotFound) + return + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(errorBody)) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + c := make(chan v1.Update, 100) + + var terr *transport.Error + if err := Write(tag, img, WithProgress(c)); err == nil { + t.Error("Write() = nil; wanted error") + } else if !errors.As(err, &terr) { + t.Errorf("Write() = %T; wanted *transport.Error", err) + } else if !expectedErrMsg.Match([]byte(terr.Error())) { + diff := cmp.Diff(expectedErrMsg, terr.Error()) + t.Errorf("Write(); (-want +got) = %s", diff) + } + + var last v1.Update + for update := range c { + last = update + } + if last.Error == nil { + t.Error("Progress chan didn't report error") + } +} + +func TestDockerhubScopes(t *testing.T) { + src, err := name.ParseReference("busybox") + if err != nil { + t.Fatal(err) + } + rl, err := random.Layer(1024, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + ml := &MountableLayer{ + Layer: rl, + Reference: src, + } + want := src.Scope(transport.PullScope) + + for _, s := range []string{ + "jonjohnson/busybox", + "docker.io/jonjohnson/busybox", + "index.docker.io/jonjohnson/busybox", + } { + dst, err := name.ParseReference(s) + if err != nil { + t.Fatal(err) + } + + scopes := scopesForUploadingImage(dst.Context(), []v1.Layer{ml}) + + if len(scopes) != 2 { + t.Errorf("Should have two scopes (src and dst), got %d", len(scopes)) + } else if diff := cmp.Diff(want, scopes[1]); diff != "" { + t.Errorf("TestDockerhubScopes %q: (-want +got) = %v", s, diff) + } + } +} + +func TestScopesForUploadingImage(t *testing.T) { + referenceToUpload, err := name.NewTag("example.com/sample/sample:latest", name.WeakValidation) + if err != nil { + t.Fatalf("name.NewTag() = %v", err) + } + + sameReference, err := name.NewTag("example.com/sample/sample:previous", name.WeakValidation) + if err != nil { + t.Fatalf("name.NewTag() = %v", err) + } + + anotherRepo1, err := name.NewTag("example.com/sample/another_repo1:latest", name.WeakValidation) + if err != nil { + t.Fatalf("name.NewTag() = %v", err) + } + + anotherRepo2, err := name.NewTag("example.com/sample/another_repo2:latest", name.WeakValidation) + if err != nil { + t.Fatalf("name.NewTag() = %v", err) + } + + repoOnOtherRegistry, err := name.NewTag("other-domain.com/sample/any_repo:latest", name.WeakValidation) + if err != nil { + t.Fatalf("name.NewTag() = %v", err) + } + + img := setupImage(t) + layers, err := img.Layers() + if err != nil { + t.Fatalf("img.Layers() = %v", err) + } + dummyLayer := layers[0] + + testCases := []struct { + name string + reference name.Reference + layers []v1.Layer + expected []string + }{ + { + name: "empty layers", + reference: referenceToUpload, + layers: []v1.Layer{}, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + }, + }, + { + name: "mountable layers with same reference", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: sameReference, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + }, + }, + { + name: "mountable layers with single reference with no-duplicate", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + anotherRepo1.Scope(transport.PullScope), + }, + }, + { + name: "mountable layers with single reference with duplicate", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + anotherRepo1.Scope(transport.PullScope), + }, + }, + { + name: "mountable layers with multiple references with no-duplicates", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo2, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + anotherRepo1.Scope(transport.PullScope), + anotherRepo2.Scope(transport.PullScope), + }, + }, + { + name: "mountable layers with multiple references with duplicates", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo2, + }, + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo1, + }, + &MountableLayer{ + Layer: dummyLayer, + Reference: anotherRepo2, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + anotherRepo1.Scope(transport.PullScope), + anotherRepo2.Scope(transport.PullScope), + }, + }, + { + name: "cross repository mountable layer", + reference: referenceToUpload, + layers: []v1.Layer{ + &MountableLayer{ + Layer: dummyLayer, + Reference: repoOnOtherRegistry, + }, + }, + expected: []string{ + referenceToUpload.Scope(transport.PushScope), + }, + }, + } + + for _, tc := range testCases { + actual := scopesForUploadingImage(tc.reference.Context(), tc.layers) + + if want, got := tc.expected[0], actual[0]; want != got { + t.Errorf("TestScopesForUploadingImage() %s: Wrong first scope; want %v, got %v", tc.name, want, got) + } + + less := func(a, b string) bool { + return strings.Compare(a, b) <= -1 + } + if diff := cmp.Diff(tc.expected[1:], actual[1:], cmpopts.SortSlices(less)); diff != "" { + t.Errorf("TestScopesForUploadingImage() %s: Wrong scopes (-want +got) = %v", tc.name, diff) + } + } +} + +func TestCheckExistingManifest(t *testing.T) { + tests := []struct { + name string + status int + existing bool + wantErr bool + }{{ + name: "success", + status: http.StatusOK, + existing: true, + }, { + name: "not found", + status: http.StatusNotFound, + existing: false, + }, { + name: "error", + status: http.StatusInternalServerError, + existing: false, + wantErr: true, + }} + + img := setupImage(t) + h := mustDigest(t, img) + mt := mustMediaType(t, img) + expectedRepo := "foo/bar" + expectedPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, h.String()) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + w, closer, err := setupWriter(expectedRepo, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodHead { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodHead) + } + if r.URL.Path != expectedPath { + t.Errorf("URL; got %v, want %v", r.URL.Path, expectedPath) + } + if got, want := r.Header.Get("Accept"), string(mt); got != want { + t.Errorf("r.Header['Accept']; got %v, want %v", got, want) + } + http.Error(w, http.StatusText(test.status), test.status) + })) + if err != nil { + t.Fatalf("setupWriter() = %v", err) + } + defer closer.Close() + + existing, err := w.checkExistingManifest(context.Background(), h, mt) + if test.existing != existing { + t.Errorf("checkExistingManifest() = %v, want %v", existing, test.existing) + } + if err != nil && !test.wantErr { + t.Errorf("checkExistingManifest() = %v", err) + } else if err == nil && test.wantErr { + t.Error("checkExistingManifest() wanted err, got nil") + } + }) + } +} + +func TestWriteIndex(t *testing.T) { + idx := setupIndex(t, 2) + expectedRepo := "write/time" + headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + childDigest := mustIndexManifest(t, idx).Manifests[0].Digest + childPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, childDigest) + existinChildDigest := mustIndexManifest(t, idx).Manifests[1].Digest + existingChildPath := fmt.Sprintf("/v2/%s/manifests/%s", expectedRepo, existinChildDigest) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath { + http.Error(w, "NotFound", http.StatusNotFound) + return + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + http.Error(w, "Mounted", http.StatusCreated) + case manifestPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + case existingChildPath: + if r.Method == http.MethodHead { + http.Error(w, http.StatusText(http.StatusOK), http.StatusOK) + return + } + t.Errorf("Unexpected method; got %v, want %v", r.Method, http.MethodHead) + case childPath: + if r.Method == http.MethodHead { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := WriteIndex(tag, idx); err != nil { + t.Errorf("WriteIndex() = %v", err) + } +} + +// If we actually attempt to read the contents, this will fail the test. +type fakeForeignLayer struct { + t *testing.T +} + +func (l *fakeForeignLayer) MediaType() (types.MediaType, error) { + return types.DockerForeignLayer, nil +} + +func (l *fakeForeignLayer) Size() (int64, error) { + return 0, nil +} + +func (l *fakeForeignLayer) Digest() (v1.Hash, error) { + return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil +} + +func (l *fakeForeignLayer) DiffID() (v1.Hash, error) { + return v1.Hash{Algorithm: "sha256", Hex: strings.Repeat("a", 64)}, nil +} + +func (l *fakeForeignLayer) Compressed() (io.ReadCloser, error) { + l.t.Helper() + l.t.Errorf("foreign layer not skipped: Compressed") + return nil, nil +} + +func (l *fakeForeignLayer) Uncompressed() (io.ReadCloser, error) { + l.t.Helper() + l.t.Errorf("foreign layer not skipped: Uncompressed") + return nil, nil +} + +func TestSkipForeignLayersByDefault(t *testing.T) { + // Set up an image with a foreign layer. + base := setupImage(t) + img, err := mutate.AppendLayers(base, &fakeForeignLayer{t: t}) + if err != nil { + t.Fatal(err) + } + + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + dst := fmt.Sprintf("%s/test/foreign/upload", u.Host) + ref, err := name.ParseReference(dst) + if err != nil { + t.Fatal(err) + } + + if err := Write(ref, img); err != nil { + t.Errorf("failed to Write: %v", err) + } +} + +func TestWriteForeignLayerIfOptionSet(t *testing.T) { + // Set up an image with a foreign layer. + base := setupImage(t) + foreignLayer, err := random.Layer(1024, types.DockerForeignLayer) + if err != nil { + t.Fatal("random.Layer:", err) + } + img, err := mutate.AppendLayers(base, foreignLayer) + if err != nil { + t.Fatal(err) + } + + expectedRepo := "write/time" + headPathPrefix := fmt.Sprintf("/v2/%s/blobs/", expectedRepo) + initiatePath := fmt.Sprintf("/v2/%s/blobs/uploads/", expectedRepo) + manifestPath := fmt.Sprintf("/v2/%s/manifests/latest", expectedRepo) + uploadPath := "/upload" + commitPath := "/commit" + var numUploads int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodHead && strings.HasPrefix(r.URL.Path, headPathPrefix) && r.URL.Path != initiatePath { + http.Error(w, "NotFound", http.StatusNotFound) + return + } + switch r.URL.Path { + case "/v2/": + w.WriteHeader(http.StatusOK) + case initiatePath: + if r.Method != http.MethodPost { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPost) + } + w.Header().Set("Location", uploadPath) + http.Error(w, "Accepted", http.StatusAccepted) + case uploadPath: + if r.Method != http.MethodPatch { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPatch) + } + atomic.AddInt32(&numUploads, 1) + w.Header().Set("Location", commitPath) + http.Error(w, "Created", http.StatusCreated) + case commitPath: + http.Error(w, "Created", http.StatusCreated) + case manifestPath: + if r.Method != http.MethodPut { + t.Errorf("Method; got %v, want %v", r.Method, http.MethodPut) + } + http.Error(w, "Created", http.StatusCreated) + default: + t.Fatalf("Unexpected path: %v", r.URL.Path) + } + })) + defer server.Close() + u, err := url.Parse(server.URL) + if err != nil { + t.Fatalf("url.Parse(%v) = %v", server.URL, err) + } + tag, err := name.NewTag(fmt.Sprintf("%s/%s:latest", u.Host, expectedRepo), name.WeakValidation) + if err != nil { + t.Fatalf("NewTag() = %v", err) + } + + if err := Write(tag, img, WithNondistributable); err != nil { + t.Errorf("Write: %v", err) + } + + // 1 random layer, 1 foreign layer, 1 image config blob + wantUploads := int32(1 + 1 + 1) + if numUploads != wantUploads { + t.Fatalf("Write uploaded %d blobs, want %d", numUploads, wantUploads) + } +} + +func TestTag(t *testing.T) { + idx := setupIndex(t, 3) + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + src := fmt.Sprintf("%s/test/tag:src", u.Host) + srcRef, err := name.NewTag(src) + if err != nil { + t.Fatal(err) + } + + if err := WriteIndex(srcRef, idx); err != nil { + t.Fatal(err) + } + + dst := fmt.Sprintf("%s/test/tag:dst", u.Host) + dstRef, err := name.NewTag(dst) + if err != nil { + t.Fatal(err) + } + + if err := Tag(dstRef, idx); err != nil { + t.Fatal(err) + } + + got, err := Index(dstRef) + if err != nil { + t.Fatal(err) + } + + if err := validate.Index(got); err != nil { + t.Errorf("Validate() = %v", err) + } +} + +func TestTagDescriptor(t *testing.T) { + idx := setupIndex(t, 3) + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + src := fmt.Sprintf("%s/test/tag:src", u.Host) + srcRef, err := name.NewTag(src) + if err != nil { + t.Fatal(err) + } + + if err := WriteIndex(srcRef, idx); err != nil { + t.Fatal(err) + } + + desc, err := Get(srcRef) + if err != nil { + t.Fatal(err) + } + + dst := fmt.Sprintf("%s/test/tag:dst", u.Host) + dstRef, err := name.NewTag(dst) + if err != nil { + t.Fatal(err) + } + + if err := Tag(dstRef, desc); err != nil { + t.Fatal(err) + } +} + +func TestNestedIndex(t *testing.T) { + // Set up a fake registry. + s := httptest.NewServer(registry.New()) + defer s.Close() + u, err := url.Parse(s.URL) + if err != nil { + t.Fatal(err) + } + src := fmt.Sprintf("%s/test/tag:src", u.Host) + srcRef, err := name.NewTag(src) + if err != nil { + t.Fatal(err) + } + + child, err := random.Index(1024, 1, 1) + if err != nil { + t.Fatal(err) + } + parent := mutate.AppendManifests(empty.Index, mutate.IndexAddendum{ + Add: child, + Descriptor: v1.Descriptor{ + URLs: []string{"example.com/url"}, + }, + }) + + l, err := random.Layer(100, types.DockerLayer) + if err != nil { + t.Fatal(err) + } + + parent = mutate.AppendManifests(parent, mutate.IndexAddendum{ + Add: l, + }) + + if err := WriteIndex(srcRef, parent); err != nil { + t.Fatal(err) + } + pulled, err := Index(srcRef) + if err != nil { + t.Fatal(err) + } + + if err := validate.Index(pulled); err != nil { + t.Fatalf("validate.Index: %v", err) + } + + digest, err := child.Digest() + if err != nil { + t.Fatal(err) + } + + pulledChild, err := pulled.ImageIndex(digest) + if err != nil { + t.Fatal(err) + } + + desc, err := partial.Descriptor(pulledChild) + if err != nil { + t.Fatal(err) + } + + if len(desc.URLs) != 1 { + t.Fatalf("expected url for pulledChild") + } + + if want, got := "example.com/url", desc.URLs[0]; want != got { + t.Errorf("pulledChild.urls[0] = %s != %s", got, want) + } +} + +func BenchmarkWrite(b *testing.B) { + // unfortunately the registry _and_ the img have caching behaviour, so we need a new registry + // and image every iteration of benchmarking. + for i := 0; i < b.N; i++ { + // set up the registry + s := httptest.NewServer(registry.New()) + defer s.Close() + + // load the image + img, err := random.Image(50*1024*1024, 10) + if err != nil { + b.Fatalf("random.Image(...): %v", err) + } + + b.ResetTimer() + + tagStr := strings.TrimPrefix(s.URL+"/test/image:tag", "http://") + tag, err := name.NewTag(tagStr) + if err != nil { + b.Fatalf("parsing tag (%s): %v", tagStr, err) + } + + err = Write(tag, img) + if err != nil { + b.Fatalf("pushing tag one: %v", err) + } + } +} diff --git a/pkg/v1/static/layer.go b/pkg/v1/static/layer.go new file mode 100644 index 0000000..a4bbe69 --- /dev/null +++ b/pkg/v1/static/layer.go @@ -0,0 +1,68 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package static + +import ( + "bytes" + "io" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// NewLayer returns a layer containing the given bytes, with the given mediaType. +// +// Contents will not be compressed. +func NewLayer(b []byte, mt types.MediaType) v1.Layer { + return &staticLayer{b: b, mt: mt} +} + +type staticLayer struct { + b []byte + mt types.MediaType + + once sync.Once + h v1.Hash +} + +func (l *staticLayer) Digest() (v1.Hash, error) { + var err error + // Only calculate digest the first time we're asked. + l.once.Do(func() { + l.h, _, err = v1.SHA256(bytes.NewReader(l.b)) + }) + return l.h, err +} + +func (l *staticLayer) DiffID() (v1.Hash, error) { + return l.Digest() +} + +func (l *staticLayer) Compressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(l.b)), nil +} + +func (l *staticLayer) Uncompressed() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(l.b)), nil +} + +func (l *staticLayer) Size() (int64, error) { + return int64(len(l.b)), nil +} + +func (l *staticLayer) MediaType() (types.MediaType, error) { + return l.mt, nil +} diff --git a/pkg/v1/static/static_test.go b/pkg/v1/static/static_test.go new file mode 100644 index 0000000..1dcc86a --- /dev/null +++ b/pkg/v1/static/static_test.go @@ -0,0 +1,83 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package static + +import ( + "io" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestNewLayer(t *testing.T) { + b := []byte(strings.Repeat(".", 10)) + l := NewLayer(b, types.OCILayer) + + // This does basically nothing. + if err := validate.Layer(l, validate.Fast); err != nil { + t.Fatal(err) + } + + // Digest and DiffID match, and match expectations. + h, err := l.Digest() + if err != nil { + t.Fatal(err) + } + h2, err := l.DiffID() + if err != nil { + t.Fatal(err) + } + if h != h2 { + t.Errorf("Digest != DiffID; digest is %v, diffid is %v", h, h2) + } + wantDigest, err := v1.NewHash("sha256:537f3fb69ba01fc388a3a5c920c485b2873d5f327305c3dd2004d6a04451659b") + if err != nil { + t.Fatal(err) + } + if h != wantDigest { + t.Errorf("Digest mismatch; got %v, want %v", h, wantDigest) + } + + sz, err := l.Size() + if err != nil { + t.Fatal(err) + } + if sz != 10 { + t.Errorf("Size mismatch; got %d, want %d", sz, 10) + } + + mt, err := l.MediaType() + if err != nil { + t.Fatal(err) + } + if mt != types.OCILayer { + t.Errorf("MediaType mismatch; got %v, want %v", mt, types.OCILayer) + } + + r, err := l.Uncompressed() + if err != nil { + t.Fatal(err) + } + got, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + if string(got) != string(b) { + t.Errorf("Contents mismatch: got %q, want %q", string(got), string(b)) + } +} diff --git a/pkg/v1/stream/README.md b/pkg/v1/stream/README.md new file mode 100644 index 0000000..da0dda4 --- /dev/null +++ b/pkg/v1/stream/README.md @@ -0,0 +1,68 @@ +# `stream` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/stream) + +The `stream` package contains an implementation of +[`v1.Layer`](https://godoc.org/github.com/google/go-containerregistry/pkg/v1#Layer) +that supports _streaming_ access, i.e. the layer contents are read once and not +buffered. + +## Usage + +```go +package main + +import ( + "os" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/stream" +) + +// upload the contents of stdin as a layer to a local registry +func main() { + repo, err := name.NewRepository("localhost:5000/stream") + if err != nil { + panic(err) + } + + layer := stream.NewLayer(os.Stdin) + + if err := remote.WriteLayer(repo, layer); err != nil { + panic(err) + } +} +``` + +## Structure + +This implements the layer portion of an [image +upload](/pkg/v1/remote#anatomy-of-an-image-upload). We launch a goroutine that +is responsible for hashing the uncompressed contents to compute the `DiffID`, +gzipping them to produce the `Compressed` contents, and hashing/counting the +bytes to produce the `Digest`/`Size`. This goroutine writes to an +`io.PipeWriter`, which blocks until `Compressed` reads the gzipped contents from +the corresponding `io.PipeReader`. + +<p align="center"> + <img src="/images/stream.dot.svg" /> +</p> + +## Caveats + +This assumes that you have an uncompressed layer (i.e. a tarball) and would like +to compress it. Calling `Uncompressed` is always an error. Likewise, other +methods are invalid until the contents of `Compressed` have been completely +consumed and `Close`d. + +Using a `stream.Layer` will likely not work without careful consideration. For +example, in the `mutate` package, we defer computing the manifest and config +file until they are actually called. This allows you to `mutate.Append` a +streaming layer to an image without accidentally consuming it. Similarly, in +`remote.Write`, if calling `Digest` on a layer fails, we attempt to upload the +layer anyway, understanding that we may be dealing with a `stream.Layer` whose +contents need to be uploaded before we can upload the config file. + +Given the [structure](#structure) of how this is implemented, forgetting to +`Close` a `stream.Layer` will leak a goroutine. diff --git a/pkg/v1/stream/layer.go b/pkg/v1/stream/layer.go new file mode 100644 index 0000000..d6f2df8 --- /dev/null +++ b/pkg/v1/stream/layer.go @@ -0,0 +1,273 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package stream implements a single-pass streaming v1.Layer. +package stream + +import ( + "bufio" + "compress/gzip" + "crypto" + "encoding/hex" + "errors" + "hash" + "io" + "os" + "sync" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +var ( + // ErrNotComputed is returned when the requested value is not yet + // computed because the stream has not been consumed yet. + ErrNotComputed = errors.New("value not computed until stream is consumed") + + // ErrConsumed is returned by Compressed when the underlying stream has + // already been consumed and closed. + ErrConsumed = errors.New("stream was already consumed") +) + +// Layer is a streaming implementation of v1.Layer. +type Layer struct { + blob io.ReadCloser + consumed bool + compression int + + mu sync.Mutex + digest, diffID *v1.Hash + size int64 + mediaType types.MediaType +} + +var _ v1.Layer = (*Layer)(nil) + +// LayerOption applies options to layer +type LayerOption func(*Layer) + +// WithCompressionLevel sets the gzip compression. See `gzip.NewWriterLevel` for possible values. +func WithCompressionLevel(level int) LayerOption { + return func(l *Layer) { + l.compression = level + } +} + +// WithMediaType is a functional option for overriding the layer's media type. +func WithMediaType(mt types.MediaType) LayerOption { + return func(l *Layer) { + l.mediaType = mt + } +} + +// NewLayer creates a Layer from an io.ReadCloser. +func NewLayer(rc io.ReadCloser, opts ...LayerOption) *Layer { + layer := &Layer{ + blob: rc, + compression: gzip.BestSpeed, + // We use DockerLayer for now as uncompressed layers + // are unimplemented + mediaType: types.DockerLayer, + } + + for _, opt := range opts { + opt(layer) + } + + return layer +} + +// Digest implements v1.Layer. +func (l *Layer) Digest() (v1.Hash, error) { + l.mu.Lock() + defer l.mu.Unlock() + if l.digest == nil { + return v1.Hash{}, ErrNotComputed + } + return *l.digest, nil +} + +// DiffID implements v1.Layer. +func (l *Layer) DiffID() (v1.Hash, error) { + l.mu.Lock() + defer l.mu.Unlock() + if l.diffID == nil { + return v1.Hash{}, ErrNotComputed + } + return *l.diffID, nil +} + +// Size implements v1.Layer. +func (l *Layer) Size() (int64, error) { + l.mu.Lock() + defer l.mu.Unlock() + if l.size == 0 { + return 0, ErrNotComputed + } + return l.size, nil +} + +// MediaType implements v1.Layer +func (l *Layer) MediaType() (types.MediaType, error) { + return l.mediaType, nil +} + +// Uncompressed implements v1.Layer. +func (l *Layer) Uncompressed() (io.ReadCloser, error) { + return nil, errors.New("NYI: stream.Layer.Uncompressed is not implemented") +} + +// Compressed implements v1.Layer. +func (l *Layer) Compressed() (io.ReadCloser, error) { + if l.consumed { + return nil, ErrConsumed + } + return newCompressedReader(l) +} + +// finalize sets the layer to consumed and computes all hash and size values. +func (l *Layer) finalize(uncompressed, compressed hash.Hash, size int64) error { + l.mu.Lock() + defer l.mu.Unlock() + + diffID, err := v1.NewHash("sha256:" + hex.EncodeToString(uncompressed.Sum(nil))) + if err != nil { + return err + } + l.diffID = &diffID + + digest, err := v1.NewHash("sha256:" + hex.EncodeToString(compressed.Sum(nil))) + if err != nil { + return err + } + l.digest = &digest + + l.size = size + l.consumed = true + return nil +} + +type compressedReader struct { + pr io.Reader + closer func() error +} + +func newCompressedReader(l *Layer) (*compressedReader, error) { + // Collect digests of compressed and uncompressed stream and size of + // compressed stream. + h := crypto.SHA256.New() + zh := crypto.SHA256.New() + count := &countWriter{} + + // gzip.Writer writes to the output stream via pipe, a hasher to + // capture compressed digest, and a countWriter to capture compressed + // size. + pr, pw := io.Pipe() + + // Write compressed bytes to be read by the pipe.Reader, hashed by zh, and counted by count. + mw := io.MultiWriter(pw, zh, count) + + // Buffer the output of the gzip writer so we don't have to wait on pr to keep writing. + // 64K ought to be small enough for anybody. + bw := bufio.NewWriterSize(mw, 2<<16) + zw, err := gzip.NewWriterLevel(bw, l.compression) + if err != nil { + return nil, err + } + + doneDigesting := make(chan struct{}) + + cr := &compressedReader{ + pr: pr, + closer: func() error { + // Immediately close pw without error. There are three ways to get + // here. + // + // 1. There was a copy error due from the underlying reader, in which + // case the error will not be overwritten. + // 2. Copying from the underlying reader completed successfully. + // 3. Close has been called before the underlying reader has been + // fully consumed. In this case pw must be closed in order to + // keep the flush of bw from blocking indefinitely. + // + // NOTE: pw.Close never returns an error. The signature is only to + // implement io.Closer. + _ = pw.Close() + + // Close the inner ReadCloser. + // + // NOTE: net/http will call close on success, so if we've already + // closed the inner rc, it's not an error. + if err := l.blob.Close(); err != nil && !errors.Is(err, os.ErrClosed) { + return err + } + + // Finalize layer with its digest and size values. + <-doneDigesting + return l.finalize(h, zh, count.n) + }, + } + go func() { + // Copy blob into the gzip writer, which also hashes and counts the + // size of the compressed output, and hasher of the raw contents. + _, copyErr := io.Copy(io.MultiWriter(h, zw), l.blob) + + // Close the gzip writer once copying is done. If this is done in the + // Close method of compressedReader instead, then it can cause a panic + // when the compressedReader is closed before the blob is fully + // consumed and io.Copy in this goroutine is still blocking. + closeErr := zw.Close() + + // Check errors from writing and closing streams. + if copyErr != nil { + close(doneDigesting) + pw.CloseWithError(copyErr) + return + } + if closeErr != nil { + close(doneDigesting) + pw.CloseWithError(closeErr) + return + } + + // Flush the buffer once all writes are complete to the gzip writer. + if err := bw.Flush(); err != nil { + close(doneDigesting) + pw.CloseWithError(err) + return + } + + // Notify closer that digests are done being written. + close(doneDigesting) + + // Close the compressed reader to calculate digest/diffID/size. This + // will cause pr to return EOF which will cause readers of the + // Compressed stream to finish reading. + pw.CloseWithError(cr.Close()) + }() + + return cr, nil +} + +func (cr *compressedReader) Read(b []byte) (int, error) { return cr.pr.Read(b) } + +func (cr *compressedReader) Close() error { return cr.closer() } + +// countWriter counts bytes written to it. +type countWriter struct{ n int64 } + +func (c *countWriter) Write(p []byte) (int, error) { + c.n += int64(len(p)) + return len(p), nil +} diff --git a/pkg/v1/stream/layer_test.go b/pkg/v1/stream/layer_test.go new file mode 100644 index 0000000..e65452b --- /dev/null +++ b/pkg/v1/stream/layer_test.go @@ -0,0 +1,298 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package stream + +import ( + "archive/tar" + "bytes" + "crypto/rand" + "errors" + "fmt" + "io" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestStreamVsBuffer(t *testing.T) { + var n, wantSize int64 = 10000, 49 + newBlob := func() io.ReadCloser { return io.NopCloser(bytes.NewReader(bytes.Repeat([]byte{'a'}, int(n)))) } + wantDigest := "sha256:3d7c465be28d9e1ed810c42aeb0e747b44441424f566722ba635dc93c947f30e" + wantDiffID := "sha256:27dd1f61b867b6a0f6e9d8a41c43231de52107e53ae424de8f847b821db4b711" + + // Check that streaming some content results in the expected digest/diffID/size. + l := NewLayer(newBlob()) + if c, err := l.Compressed(); err != nil { + t.Errorf("Compressed: %v", err) + } else { + if _, err := io.Copy(io.Discard, c); err != nil { + t.Errorf("error reading Compressed: %v", err) + } + if err := c.Close(); err != nil { + t.Errorf("Close: %v", err) + } + } + if d, err := l.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if d.String() != wantDigest { + t.Errorf("stream Digest got %q, want %q", d.String(), wantDigest) + } + if d, err := l.DiffID(); err != nil { + t.Errorf("DiffID: %v", err) + } else if d.String() != wantDiffID { + t.Errorf("stream DiffID got %q, want %q", d.String(), wantDiffID) + } + if s, err := l.Size(); err != nil { + t.Errorf("Size: %v", err) + } else if s != wantSize { + t.Errorf("stream Size got %d, want %d", s, wantSize) + } + + // Test that buffering the same contents and using + // tarball.LayerFromOpener results in the same digest/diffID/size. + tl, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { return newBlob(), nil }) + if err != nil { + t.Fatalf("LayerFromOpener: %v", err) + } + if d, err := tl.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if d.String() != wantDigest { + t.Errorf("tarball Digest got %q, want %q", d.String(), wantDigest) + } + if d, err := tl.DiffID(); err != nil { + t.Errorf("DiffID: %v", err) + } else if d.String() != wantDiffID { + t.Errorf("tarball DiffID got %q, want %q", d.String(), wantDiffID) + } + if s, err := tl.Size(); err != nil { + t.Errorf("Size: %v", err) + } else if s != wantSize { + t.Errorf("stream Size got %d, want %d", s, wantSize) + } + + // Test with different compression + l2 := NewLayer(newBlob(), WithCompressionLevel(2)) + l2WantDigest := "sha256:c9afe7b0da6783232e463e12328cb306142548384accf3995806229c9a6a707f" + if c, err := l2.Compressed(); err != nil { + t.Errorf("Compressed: %v", err) + } else { + if _, err := io.Copy(io.Discard, c); err != nil { + t.Errorf("error reading Compressed: %v", err) + } + if err := c.Close(); err != nil { + t.Errorf("Close: %v", err) + } + } + if d, err := l2.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if d.String() != l2WantDigest { + t.Errorf("stream Digest got %q, want %q", d.String(), l2WantDigest) + } +} + +func TestLargeStream(t *testing.T) { + var n, wantSize int64 = 10000000, 10000788 // "Compressing" n random bytes results in this many bytes. + sl := NewLayer(io.NopCloser(io.LimitReader(rand.Reader, n))) + rc, err := sl.Compressed() + if err != nil { + t.Fatalf("Uncompressed: %v", err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Fatalf("Reading layer: %v", err) + } + if err := rc.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + if dig, err := sl.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if dig.String() == (v1.Hash{}).String() { + t.Errorf("Digest got %q, want anything else", (v1.Hash{}).String()) + } + if diffID, err := sl.DiffID(); err != nil { + t.Errorf("DiffID: %v", err) + } else if diffID.String() == (v1.Hash{}).String() { + t.Errorf("DiffID got %q, want anything else", (v1.Hash{}).String()) + } + if size, err := sl.Size(); err != nil { + t.Errorf("Size: %v", err) + } else if size != wantSize { + t.Errorf("Size got %d, want %d", size, wantSize) + } +} + +func TestStreamableLayerFromTarball(t *testing.T) { + pr, pw := io.Pipe() + tw := tar.NewWriter(pw) + go func() { + // "Stream" a bunch of files into the layer. + pw.CloseWithError(func() error { + for i := 0; i < 1000; i++ { + name := fmt.Sprintf("file-%d.txt", i) + body := fmt.Sprintf("i am file number %d", i) + if err := tw.WriteHeader(&tar.Header{ + Name: name, + Mode: 0600, + Size: int64(len(body)), + Typeflag: tar.TypeReg, + }); err != nil { + return err + } + if _, err := tw.Write([]byte(body)); err != nil { + return err + } + } + if err := tw.Close(); err != nil { + return err + } + return nil + }()) + }() + + l := NewLayer(pr) + rc, err := l.Compressed() + if err != nil { + t.Fatalf("Compressed: %v", err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Fatalf("Copy: %v", err) + } + if err := rc.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + wantDigest := "sha256:ed80efd7e7e884fb59db568f234332283b341b96155e872d638de42d55a34198" + if got, err := l.Digest(); err != nil { + t.Errorf("Digest: %v", err) + } else if got.String() != wantDigest { + t.Errorf("Digest: got %q, want %q", got.String(), wantDigest) + } +} + +// TestNotComputed tests that Digest/DiffID/Size return ErrNotComputed before +// the stream has been consumed. +func TestNotComputed(t *testing.T) { + l := NewLayer(io.NopCloser(bytes.NewBufferString("hi"))) + + // All methods should return ErrNotComputed until the stream has been + // consumed and closed. + if _, err := l.Size(); !errors.Is(err, ErrNotComputed) { + t.Errorf("Size: got %v, want %v", err, ErrNotComputed) + } + if _, err := l.Digest(); err == nil { + t.Errorf("Digest: got %v, want %v", err, ErrNotComputed) + } + if _, err := l.DiffID(); err == nil { + t.Errorf("DiffID: got %v, want %v", err, ErrNotComputed) + } +} + +// TestConsumed tests that Compressed returns ErrConsumed when the stream has +// already been consumed. +func TestConsumed(t *testing.T) { + l := NewLayer(io.NopCloser(strings.NewReader("hello"))) + rc, err := l.Compressed() + if err != nil { + t.Errorf("Compressed: %v", err) + } + if _, err := io.Copy(io.Discard, rc); err != nil { + t.Errorf("Error reading contents: %v", err) + } + if err := rc.Close(); err != nil { + t.Errorf("Close: %v", err) + } + + if _, err := l.Compressed(); !errors.Is(err, ErrConsumed) { + t.Errorf("Compressed() after consuming; got %v, want %v", err, ErrConsumed) + } +} + +func TestCloseTextStreamBeforeConsume(t *testing.T) { + // Create stream layer from tar pipe + l := NewLayer(io.NopCloser(strings.NewReader("hello"))) + rc, err := l.Compressed() + if err != nil { + t.Fatalf("Compressed: %v", err) + } + + // Close stream layer before consuming + if err := rc.Close(); err != nil { + t.Fatalf("Close: %v", err) + } +} + +func TestCloseTarStreamBeforeConsume(t *testing.T) { + // Write small tar to pipe + pr, pw := io.Pipe() + tw := tar.NewWriter(pw) + go func() { + pw.CloseWithError(func() error { + body := "test file" + if err := tw.WriteHeader(&tar.Header{ + Name: "test.txt", + Mode: 0600, + Size: int64(len(body)), + Typeflag: tar.TypeReg, + }); err != nil { + return err + } + if _, err := tw.Write([]byte(body)); err != nil { + return err + } + return tw.Close() + }()) + }() + + // Create stream layer from tar pipe + l := NewLayer(pr) + rc, err := l.Compressed() + if err != nil { + t.Fatalf("Compressed: %v", err) + } + + // Close stream layer before consuming + if err := rc.Close(); err != nil { + t.Fatalf("Close: %v", err) + } +} + +func TestMediaType(t *testing.T) { + l := NewLayer(io.NopCloser(strings.NewReader("hello"))) + mediaType, err := l.MediaType() + + if err != nil { + t.Fatalf("MediaType(): %v", err) + } + + if got, want := mediaType, types.DockerLayer; got != want { + t.Errorf("MediaType(): want %q, got %q", want, got) + } +} + +func TestMediaTypeOption(t *testing.T) { + l := NewLayer(io.NopCloser(strings.NewReader("hello")), WithMediaType(types.OCILayer)) + mediaType, err := l.MediaType() + + if err != nil { + t.Fatalf("MediaType(): %v", err) + } + + if got, want := mediaType, types.OCILayer; got != want { + t.Errorf("MediaType(): want %q, got %q", want, got) + } +} diff --git a/pkg/v1/tarball/README.md b/pkg/v1/tarball/README.md new file mode 100644 index 0000000..03f339b --- /dev/null +++ b/pkg/v1/tarball/README.md @@ -0,0 +1,280 @@ +# `tarball` + +[![GoDoc](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball?status.svg)](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball) + +This package produces tarballs that can consumed via `docker load`. Note +that this is a _different_ format from the [`legacy`](/pkg/legacy/tarball) +tarballs that are produced by `docker save`, but this package is still able to +read the legacy tarballs produced by `docker save`. + +## Usage + +```go +package main + +import ( + "os" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func main() { + // Read a tarball from os.Args[1] that contains ubuntu. + tag, err := name.NewTag("ubuntu") + if err != nil { + panic(err) + } + img, err := tarball.ImageFromPath(os.Args[1], &tag) + if err != nil { + panic(err) + } + + // Write that tarball to os.Args[2] with a different tag. + newTag, err := name.NewTag("ubuntu:newest") + if err != nil { + panic(err) + } + f, err := os.Create(os.Args[2]) + if err != nil { + panic(err) + } + defer f.Close() + + if err := tarball.Write(newTag, img, f); err != nil { + panic(err) + } +} +``` + +## Structure + +<p align="center"> + <img src="/images/tarball.dot.svg" /> +</p> + +Let's look at what happens when we write out a tarball: + + +### `ubuntu:latest` + +``` +$ crane pull ubuntu ubuntu.tar && mkdir ubuntu && tar xf ubuntu.tar -C ubuntu && rm ubuntu.tar +$ tree ubuntu/ +ubuntu/ +├── 423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz +├── b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz +├── de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz +├── f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz +├── manifest.json +└── sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c + +0 directories, 6 files +``` + +There are a couple interesting files here. + +`manifest.json` is the entrypoint: a list of [`tarball.Descriptor`s](https://godoc.org/github.com/google/go-containerregistry/pkg/v1/tarball#Descriptor) +that describe the images contained in this tarball. + +For each image, this has the `RepoTags` (how it was pulled), a `Config` file +that points to the image's config file, a list of `Layers`, and (optionally) +`LayerSources`. + +``` +$ jq < ubuntu/manifest.json +[ + { + "Config": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c", + "RepoTags": [ + "ubuntu" + ], + "Layers": [ + "423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954.tar.gz", + "de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d.tar.gz", + "f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b.tar.gz", + "b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7.tar.gz" + ] + } +] +``` + +The config file and layers are exactly what you would expect, and match the +registry representations of the same artifacts. You'll notice that the +`manifest.json` contains similar information as the registry manifest, but isn't +quite the same: + +``` +$ crane manifest ubuntu@sha256:0925d086715714114c1988f7c947db94064fd385e171a63c07730f1fa014e6f9 +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 3408, + "digest": "sha256:72300a873c2ca11c70d0c8642177ce76ff69ae04d61a5813ef58d40ff66e3e7c" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 26692096, + "digest": "sha256:423ae2b273f4c17ceee9e8482fa8d071d90c7d052ae208e1fe4963fceb3d6954" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 35365, + "digest": "sha256:de83a2304fa1f7c4a13708a0d15b9704f5945c2be5cbb2b3ed9b2ccb718d0b3d" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 852, + "digest": "sha256:f9a83bce3af0648efaa60b9bb28225b09136d2d35d0bed25ac764297076dec1b" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 163, + "digest": "sha256:b6b53be908de2c0c78070fff0a9f04835211b3156c4e73785747af365e71a0d7" + } + ] +} +``` + +This makes it difficult to maintain image digests when roundtripping images +through the tarball format, so it's not a great format if you care about +provenance. + +The ubuntu example didn't have any `LayerSources` -- let's look at another image +that does. + +### `hello-world:nanoserver` + +``` +$ crane pull hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b nanoserver.tar +$ mkdir nanoserver && tar xf nanoserver.tar -C nanoserver && rm nanoserver.tar +$ tree nanoserver/ +nanoserver/ +├── 10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz +├── a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz +├── be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz +├── manifest.json +└── sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6 + +0 directories, 5 files + +$ jq < nanoserver/manifest.json +[ + { + "Config": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6", + "RepoTags": [ + "index.docker.io/library/hello-world:i-was-a-digest" + ], + "Layers": [ + "a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053.tar.gz", + "be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a.tar.gz", + "10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0.tar.gz" + ], + "LayerSources": { + "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 101145811, + "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053", + "urls": [ + "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053" + ] + } + } + } +] +``` + +A couple things to note about this `manifest.json` versus the other: +* The `RepoTags` field is a bit weird here. `hello-world` is a multi-platform + image, so We had to pull this image by digest, since we're (I'm) on + amd64/linux and wanted to grab a windows image. Since the tarball format + expects a tag under `RepoTags`, and we didn't pull by tag, we replace the + digest with a sentinel `i-was-a-digest` "tag" to appease docker. +* The `LayerSources` has enough information to reconstruct the foreign layers + pointer when pushing/pulling from the registry. For legal reasons, microsoft + doesn't want anyone but them to serve windows base images, so the mediaType + here indicates a "foreign" or "non-distributable" layer with an URL for where + you can download it from microsoft (see the [OCI + image-spec](https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers)). + +We can look at what's in the registry to explain both of these things: +``` +$ crane manifest hello-world:nanoserver | jq . +{ + "manifests": [ + { + "digest": "sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b", + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "platform": { + "architecture": "amd64", + "os": "windows", + "os.version": "10.0.17763.1040" + }, + "size": 1124 + } + ], + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "schemaVersion": 2 +} + + +# Note the media type and "urls" field. +$ crane manifest hello-world:nanoserver@sha256:63c287625c2b0b72900e562de73c0e381472a83b1b39217aef3856cd398eca0b | jq . +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1721, + "digest": "sha256:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 101145811, + "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053", + "urls": [ + "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053" + ] + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 1669, + "digest": "sha256:be21f08f670160cbae227e3053205b91d6bfa3de750b90c7e00bd2c511ccb63a" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 949, + "digest": "sha256:10d1439be4eb8819987ec2e9c140d44d74d6b42a823d57fe1953bd99948e1bc0" + } + ] +} +``` + +The `LayerSources` map is keyed by the diffid. Note that `sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e` matches the first layer in the config file: +``` +$ jq '.[0].LayerSources' < nanoserver/manifest.json +{ + "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e": { + "mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip", + "size": 101145811, + "digest": "sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053", + "urls": [ + "https://mcr.microsoft.com/v2/windows/nanoserver/blobs/sha256:a35da61c356213336e646756218539950461ff2bf096badf307a23add6e70053" + ] + } +} + +$ jq < nanoserver/sha256\:bc5d255ea81f83c8c38a982a6d29a6f2198427d258aea5f166e49856896b2da6 | jq .rootfs +{ + "type": "layers", + "diff_ids": [ + "sha256:26fd2d9d4c64a4f965bbc77939a454a31b607470f430b5d69fc21ded301fa55e", + "sha256:601cf7d78c62e4b4d32a7bbf96a17606a9cea5bd9d22ffa6f34aa431d056b0e8", + "sha256:a1e1a3bf6529adcce4d91dce2cad86c2604a66b507ccbc4d2239f3da0ec5aab9" + ] +} +``` diff --git a/pkg/v1/tarball/doc.go b/pkg/v1/tarball/doc.go new file mode 100644 index 0000000..4eb79bb --- /dev/null +++ b/pkg/v1/tarball/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package tarball provides facilities for reading/writing v1.Images from/to +// a tarball on-disk. +package tarball diff --git a/pkg/v1/tarball/image.go b/pkg/v1/tarball/image.go new file mode 100644 index 0000000..1f977e1 --- /dev/null +++ b/pkg/v1/tarball/image.go @@ -0,0 +1,429 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "sync" + + comp "github.com/google/go-containerregistry/internal/compression" + "github.com/google/go-containerregistry/pkg/compression" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type image struct { + opener Opener + manifest *Manifest + config []byte + imgDescriptor *Descriptor + + tag *name.Tag +} + +type uncompressedImage struct { + *image +} + +type compressedImage struct { + *image + manifestLock sync.Mutex // Protects manifest + manifest *v1.Manifest +} + +var _ partial.UncompressedImageCore = (*uncompressedImage)(nil) +var _ partial.CompressedImageCore = (*compressedImage)(nil) + +// Opener is a thunk for opening a tar file. +type Opener func() (io.ReadCloser, error) + +func pathOpener(path string) Opener { + return func() (io.ReadCloser, error) { + return os.Open(path) + } +} + +// ImageFromPath returns a v1.Image from a tarball located on path. +func ImageFromPath(path string, tag *name.Tag) (v1.Image, error) { + return Image(pathOpener(path), tag) +} + +// LoadManifest load manifest +func LoadManifest(opener Opener) (Manifest, error) { + m, err := extractFileFromTar(opener, "manifest.json") + if err != nil { + return nil, err + } + defer m.Close() + + var manifest Manifest + + if err := json.NewDecoder(m).Decode(&manifest); err != nil { + return nil, err + } + return manifest, nil +} + +// Image exposes an image from the tarball at the provided path. +func Image(opener Opener, tag *name.Tag) (v1.Image, error) { + img := &image{ + opener: opener, + tag: tag, + } + if err := img.loadTarDescriptorAndConfig(); err != nil { + return nil, err + } + + // Peek at the first layer and see if it's compressed. + if len(img.imgDescriptor.Layers) > 0 { + compressed, err := img.areLayersCompressed() + if err != nil { + return nil, err + } + if compressed { + c := compressedImage{ + image: img, + } + return partial.CompressedToImage(&c) + } + } + + uc := uncompressedImage{ + image: img, + } + return partial.UncompressedToImage(&uc) +} + +func (i *image) MediaType() (types.MediaType, error) { + return types.DockerManifestSchema2, nil +} + +// Descriptor stores the manifest data for a single image inside a `docker save` tarball. +type Descriptor struct { + Config string + RepoTags []string + Layers []string + + // Tracks foreign layer info. Key is DiffID. + LayerSources map[v1.Hash]v1.Descriptor `json:",omitempty"` +} + +// Manifest represents the manifests of all images as the `manifest.json` file in a `docker save` tarball. +type Manifest []Descriptor + +func (m Manifest) findDescriptor(tag *name.Tag) (*Descriptor, error) { + if tag == nil { + if len(m) != 1 { + return nil, errors.New("tarball must contain only a single image to be used with tarball.Image") + } + return &(m)[0], nil + } + for _, img := range m { + for _, tagStr := range img.RepoTags { + repoTag, err := name.NewTag(tagStr) + if err != nil { + return nil, err + } + + // Compare the resolved names, since there are several ways to specify the same tag. + if repoTag.Name() == tag.Name() { + return &img, nil + } + } + } + return nil, fmt.Errorf("tag %s not found in tarball", tag) +} + +func (i *image) areLayersCompressed() (bool, error) { + if len(i.imgDescriptor.Layers) == 0 { + return false, errors.New("0 layers found in image") + } + layer := i.imgDescriptor.Layers[0] + blob, err := extractFileFromTar(i.opener, layer) + if err != nil { + return false, err + } + defer blob.Close() + + cp, _, err := comp.PeekCompression(blob) + if err != nil { + return false, err + } + + return cp != compression.None, nil +} + +func (i *image) loadTarDescriptorAndConfig() error { + m, err := extractFileFromTar(i.opener, "manifest.json") + if err != nil { + return err + } + defer m.Close() + + if err := json.NewDecoder(m).Decode(&i.manifest); err != nil { + return err + } + + if i.manifest == nil { + return errors.New("no valid manifest.json in tarball") + } + + i.imgDescriptor, err = i.manifest.findDescriptor(i.tag) + if err != nil { + return err + } + + cfg, err := extractFileFromTar(i.opener, i.imgDescriptor.Config) + if err != nil { + return err + } + defer cfg.Close() + + i.config, err = io.ReadAll(cfg) + if err != nil { + return err + } + return nil +} + +func (i *image) RawConfigFile() ([]byte, error) { + return i.config, nil +} + +// tarFile represents a single file inside a tar. Closing it closes the tar itself. +type tarFile struct { + io.Reader + io.Closer +} + +func extractFileFromTar(opener Opener, filePath string) (io.ReadCloser, error) { + f, err := opener() + if err != nil { + return nil, err + } + close := true + defer func() { + if close { + f.Close() + } + }() + + tf := tar.NewReader(f) + for { + hdr, err := tf.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + if hdr.Name == filePath { + if hdr.Typeflag == tar.TypeSymlink || hdr.Typeflag == tar.TypeLink { + currentDir := filepath.Dir(filePath) + return extractFileFromTar(opener, path.Join(currentDir, path.Clean(hdr.Linkname))) + } + close = false + return tarFile{ + Reader: tf, + Closer: f, + }, nil + } + } + return nil, fmt.Errorf("file %s not found in tar", filePath) +} + +// uncompressedLayerFromTarball implements partial.UncompressedLayer +type uncompressedLayerFromTarball struct { + diffID v1.Hash + mediaType types.MediaType + opener Opener + filePath string +} + +// foreignUncompressedLayer implements partial.UncompressedLayer but returns +// a custom descriptor. This allows the foreign layer URLs to be included in +// the generated image manifest for uncompressed layers. +type foreignUncompressedLayer struct { + uncompressedLayerFromTarball + desc v1.Descriptor +} + +func (fl *foreignUncompressedLayer) Descriptor() (*v1.Descriptor, error) { + return &fl.desc, nil +} + +// DiffID implements partial.UncompressedLayer +func (ulft *uncompressedLayerFromTarball) DiffID() (v1.Hash, error) { + return ulft.diffID, nil +} + +// Uncompressed implements partial.UncompressedLayer +func (ulft *uncompressedLayerFromTarball) Uncompressed() (io.ReadCloser, error) { + return extractFileFromTar(ulft.opener, ulft.filePath) +} + +func (ulft *uncompressedLayerFromTarball) MediaType() (types.MediaType, error) { + return ulft.mediaType, nil +} + +func (i *uncompressedImage) LayerByDiffID(h v1.Hash) (partial.UncompressedLayer, error) { + cfg, err := partial.ConfigFile(i) + if err != nil { + return nil, err + } + for idx, diffID := range cfg.RootFS.DiffIDs { + if diffID == h { + // Technically the media type should be 'application/tar' but given that our + // v1.Layer doesn't force consumers to care about whether the layer is compressed + // we should be fine returning the DockerLayer media type + mt := types.DockerLayer + if bd, ok := i.imgDescriptor.LayerSources[h]; ok { + // Overwrite the mediaType for foreign layers. + return &foreignUncompressedLayer{ + uncompressedLayerFromTarball: uncompressedLayerFromTarball{ + diffID: diffID, + mediaType: bd.MediaType, + opener: i.opener, + filePath: i.imgDescriptor.Layers[idx], + }, + desc: bd, + }, nil + } + return &uncompressedLayerFromTarball{ + diffID: diffID, + mediaType: mt, + opener: i.opener, + filePath: i.imgDescriptor.Layers[idx], + }, nil + } + } + return nil, fmt.Errorf("diff id %q not found", h) +} + +func (c *compressedImage) Manifest() (*v1.Manifest, error) { + c.manifestLock.Lock() + defer c.manifestLock.Unlock() + if c.manifest != nil { + return c.manifest, nil + } + + b, err := c.RawConfigFile() + if err != nil { + return nil, err + } + + cfgHash, cfgSize, err := v1.SHA256(bytes.NewReader(b)) + if err != nil { + return nil, err + } + + c.manifest = &v1.Manifest{ + SchemaVersion: 2, + MediaType: types.DockerManifestSchema2, + Config: v1.Descriptor{ + MediaType: types.DockerConfigJSON, + Size: cfgSize, + Digest: cfgHash, + }, + } + + for i, p := range c.imgDescriptor.Layers { + cfg, err := partial.ConfigFile(c) + if err != nil { + return nil, err + } + diffid := cfg.RootFS.DiffIDs[i] + if d, ok := c.imgDescriptor.LayerSources[diffid]; ok { + // If it's a foreign layer, just append the descriptor so we can avoid + // reading the entire file. + c.manifest.Layers = append(c.manifest.Layers, d) + } else { + l, err := extractFileFromTar(c.opener, p) + if err != nil { + return nil, err + } + defer l.Close() + sha, size, err := v1.SHA256(l) + if err != nil { + return nil, err + } + c.manifest.Layers = append(c.manifest.Layers, v1.Descriptor{ + MediaType: types.DockerLayer, + Size: size, + Digest: sha, + }) + } + } + return c.manifest, nil +} + +func (c *compressedImage) RawManifest() ([]byte, error) { + return partial.RawManifest(c) +} + +// compressedLayerFromTarball implements partial.CompressedLayer +type compressedLayerFromTarball struct { + desc v1.Descriptor + opener Opener + filePath string +} + +// Digest implements partial.CompressedLayer +func (clft *compressedLayerFromTarball) Digest() (v1.Hash, error) { + return clft.desc.Digest, nil +} + +// Compressed implements partial.CompressedLayer +func (clft *compressedLayerFromTarball) Compressed() (io.ReadCloser, error) { + return extractFileFromTar(clft.opener, clft.filePath) +} + +// MediaType implements partial.CompressedLayer +func (clft *compressedLayerFromTarball) MediaType() (types.MediaType, error) { + return clft.desc.MediaType, nil +} + +// Size implements partial.CompressedLayer +func (clft *compressedLayerFromTarball) Size() (int64, error) { + return clft.desc.Size, nil +} + +func (c *compressedImage) LayerByDigest(h v1.Hash) (partial.CompressedLayer, error) { + m, err := c.Manifest() + if err != nil { + return nil, err + } + for i, l := range m.Layers { + if l.Digest == h { + fp := c.imgDescriptor.Layers[i] + return &compressedLayerFromTarball{ + desc: l, + opener: c.opener, + filePath: fp, + }, nil + } + } + return nil, fmt.Errorf("blob %v not found", h) +} diff --git a/pkg/v1/tarball/image_test.go b/pkg/v1/tarball/image_test.go new file mode 100644 index 0000000..3a46400 --- /dev/null +++ b/pkg/v1/tarball/image_test.go @@ -0,0 +1,139 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "io" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestManifestAndConfig(t *testing.T) { + img, err := ImageFromPath("testdata/test_image_1.tar", nil) + if err != nil { + t.Fatalf("Error loading image: %v", err) + } + manifest, err := img.Manifest() + if err != nil { + t.Fatalf("Error loading manifest: %v", err) + } + if len(manifest.Layers) != 1 { + t.Fatalf("layers should be 1, got %d", len(manifest.Layers)) + } + + config, err := img.ConfigFile() + if err != nil { + t.Fatalf("Error loading config file: %v", err) + } + if len(config.History) != 1 { + t.Fatalf("history length should be 1, got %d", len(config.History)) + } + + if err := validate.Image(img); err != nil { + t.Errorf("Validate() = %v", err) + } +} + +func TestNullManifest(t *testing.T) { + img, err := ImageFromPath("testdata/null_manifest.tar", nil) + if err == nil { + t.Fatalf("Error expected loading null image: %v", img) + } +} + +func TestNoManifest(t *testing.T) { + img, err := ImageFromPath("testdata/no_manifest.tar", nil) + if err == nil { + t.Fatalf("Error expected loading image: %v", img) + } +} + +func TestBundleSingle(t *testing.T) { + img, err := ImageFromPath("testdata/test_bundle.tar", nil) + if err == nil { + t.Fatalf("Error expected loading image: %v", img) + } +} + +func TestBundleMultiple(t *testing.T) { + for _, imgName := range []string{ + "test_image_1", + "test_image_2", + "test_image_1:latest", + "test_image_2:latest", + "index.docker.io/library/test_image_1:latest", + } { + t.Run(imgName, func(t *testing.T) { + tag, err := name.NewTag(imgName, name.WeakValidation) + if err != nil { + t.Fatalf("Error creating tag: %v", err) + } + img, err := ImageFromPath("testdata/test_bundle.tar", &tag) + if err != nil { + t.Fatalf("Error loading image: %v", err) + } + if _, err := img.Manifest(); err != nil { + t.Fatalf("Unexpected error loading manifest: %v", err) + } + + if err := validate.Image(img); err != nil { + t.Errorf("Validate() = %v", err) + } + }) + } +} + +func TestLayerLink(t *testing.T) { + tag, err := name.NewTag("bazel/v1/tarball:test_image_3", name.WeakValidation) + if err != nil { + t.Fatalf("Error creating tag: %v", err) + } + img, err := ImageFromPath("testdata/test_link.tar", &tag) + if err != nil { + t.Fatalf("Error loading image: %v", img) + } + hash := v1.Hash{ + Algorithm: "sha256", + Hex: "8897395fd26dc44ad0e2a834335b33198cb41ac4d98dfddf58eced3853fa7b17", + } + layer, err := img.LayerByDiffID(hash) + if err != nil { + t.Fatalf("Error getting layer by diff ID: %v, %v", hash, err) + } + rc, err := layer.Uncompressed() + if err != nil { + t.Fatal(err) + } + bs, err := io.ReadAll(rc) + if err != nil { + t.Fatal(err) + } + if len(bs) == 0 { + t.Errorf("layer.Uncompressed() returned a link file") + } +} + +func TestLoadManifest(t *testing.T) { + manifest, err := LoadManifest(pathOpener("testdata/test_load_manifest.tar")) + if err != nil { + t.Fatalf("Error load manifest: %v", err) + } + if len(manifest) == 0 { + t.Fatalf("get nothing") + } +} diff --git a/pkg/v1/tarball/layer.go b/pkg/v1/tarball/layer.go new file mode 100644 index 0000000..a344e92 --- /dev/null +++ b/pkg/v1/tarball/layer.go @@ -0,0 +1,349 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "os" + "sync" + + "github.com/containerd/stargz-snapshotter/estargz" + "github.com/google/go-containerregistry/internal/and" + comp "github.com/google/go-containerregistry/internal/compression" + gestargz "github.com/google/go-containerregistry/internal/estargz" + ggzip "github.com/google/go-containerregistry/internal/gzip" + "github.com/google/go-containerregistry/internal/zstd" + "github.com/google/go-containerregistry/pkg/compression" + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type layer struct { + digest v1.Hash + diffID v1.Hash + size int64 + compressedopener Opener + uncompressedopener Opener + compression compression.Compression + compressionLevel int + annotations map[string]string + estgzopts []estargz.Option + mediaType types.MediaType +} + +// Descriptor implements partial.withDescriptor. +func (l *layer) Descriptor() (*v1.Descriptor, error) { + digest, err := l.Digest() + if err != nil { + return nil, err + } + return &v1.Descriptor{ + Size: l.size, + Digest: digest, + Annotations: l.annotations, + MediaType: l.mediaType, + }, nil +} + +// Digest implements v1.Layer +func (l *layer) Digest() (v1.Hash, error) { + return l.digest, nil +} + +// DiffID implements v1.Layer +func (l *layer) DiffID() (v1.Hash, error) { + return l.diffID, nil +} + +// Compressed implements v1.Layer +func (l *layer) Compressed() (io.ReadCloser, error) { + return l.compressedopener() +} + +// Uncompressed implements v1.Layer +func (l *layer) Uncompressed() (io.ReadCloser, error) { + return l.uncompressedopener() +} + +// Size implements v1.Layer +func (l *layer) Size() (int64, error) { + return l.size, nil +} + +// MediaType implements v1.Layer +func (l *layer) MediaType() (types.MediaType, error) { + return l.mediaType, nil +} + +// LayerOption applies options to layer +type LayerOption func(*layer) + +// WithCompression is a functional option for overriding the default +// compression algorithm used for compressing uncompressed tarballs. +// Please note that WithCompression(compression.ZStd) should be used +// in conjunction with WithMediaType(types.OCILayerZStd) +func WithCompression(comp compression.Compression) LayerOption { + return func(l *layer) { + switch comp { + case compression.ZStd: + l.compression = compression.ZStd + case compression.GZip: + l.compression = compression.GZip + case compression.None: + logs.Warn.Printf("Compression type 'none' is not supported for tarball layers; using gzip compression.") + l.compression = compression.GZip + default: + logs.Warn.Printf("Unexpected compression type for WithCompression(): %s; using gzip compression instead.", comp) + l.compression = compression.GZip + } + } +} + +// WithCompressionLevel is a functional option for overriding the default +// compression level used for compressing uncompressed tarballs. +func WithCompressionLevel(level int) LayerOption { + return func(l *layer) { + l.compressionLevel = level + } +} + +// WithMediaType is a functional option for overriding the layer's media type. +func WithMediaType(mt types.MediaType) LayerOption { + return func(l *layer) { + l.mediaType = mt + } +} + +// WithCompressedCaching is a functional option that overrides the +// logic for accessing the compressed bytes to memoize the result +// and avoid expensive repeated gzips. +func WithCompressedCaching(l *layer) { + var once sync.Once + var err error + + buf := bytes.NewBuffer(nil) + og := l.compressedopener + + l.compressedopener = func() (io.ReadCloser, error) { + once.Do(func() { + var rc io.ReadCloser + rc, err = og() + if err == nil { + defer rc.Close() + _, err = io.Copy(buf, rc) + } + }) + if err != nil { + return nil, err + } + + return io.NopCloser(bytes.NewBuffer(buf.Bytes())), nil + } +} + +// WithEstargzOptions is a functional option that allow the caller to pass +// through estargz.Options to the underlying compression layer. This is +// only meaningful when estargz is enabled. +func WithEstargzOptions(opts ...estargz.Option) LayerOption { + return func(l *layer) { + l.estgzopts = opts + } +} + +// WithEstargz is a functional option that explicitly enables estargz support. +func WithEstargz(l *layer) { + oguncompressed := l.uncompressedopener + estargz := func() (io.ReadCloser, error) { + crc, err := oguncompressed() + if err != nil { + return nil, err + } + eopts := append(l.estgzopts, estargz.WithCompressionLevel(l.compressionLevel)) + rc, h, err := gestargz.ReadCloser(crc, eopts...) + if err != nil { + return nil, err + } + l.annotations[estargz.TOCJSONDigestAnnotation] = h.String() + return &and.ReadCloser{ + Reader: rc, + CloseFunc: func() error { + err := rc.Close() + if err != nil { + return err + } + // As an optimization, leverage the DiffID exposed by the estargz ReadCloser + l.diffID, err = v1.NewHash(rc.DiffID().String()) + return err + }, + }, nil + } + uncompressed := func() (io.ReadCloser, error) { + urc, err := estargz() + if err != nil { + return nil, err + } + return ggzip.UnzipReadCloser(urc) + } + + l.compressedopener = estargz + l.uncompressedopener = uncompressed +} + +// LayerFromFile returns a v1.Layer given a tarball +func LayerFromFile(path string, opts ...LayerOption) (v1.Layer, error) { + opener := func() (io.ReadCloser, error) { + return os.Open(path) + } + return LayerFromOpener(opener, opts...) +} + +// LayerFromOpener returns a v1.Layer given an Opener function. +// The Opener may return either an uncompressed tarball (common), +// or a compressed tarball (uncommon). +// +// When using this in conjunction with something like remote.Write +// the uncompressed path may end up gzipping things multiple times: +// 1. Compute the layer SHA256 +// 2. Upload the compressed layer. +// +// Since gzip can be expensive, we support an option to memoize the +// compression that can be passed here: tarball.WithCompressedCaching +func LayerFromOpener(opener Opener, opts ...LayerOption) (v1.Layer, error) { + comp, err := comp.GetCompression(opener) + if err != nil { + return nil, err + } + + layer := &layer{ + compression: compression.GZip, + compressionLevel: gzip.BestSpeed, + annotations: make(map[string]string, 1), + mediaType: types.DockerLayer, + } + + if estgz := os.Getenv("GGCR_EXPERIMENT_ESTARGZ"); estgz == "1" { + opts = append([]LayerOption{WithEstargz}, opts...) + } + + switch comp { + case compression.GZip: + layer.compressedopener = opener + layer.uncompressedopener = func() (io.ReadCloser, error) { + urc, err := opener() + if err != nil { + return nil, err + } + return ggzip.UnzipReadCloser(urc) + } + case compression.ZStd: + layer.compressedopener = opener + layer.uncompressedopener = func() (io.ReadCloser, error) { + urc, err := opener() + if err != nil { + return nil, err + } + return zstd.UnzipReadCloser(urc) + } + default: + layer.uncompressedopener = opener + layer.compressedopener = func() (io.ReadCloser, error) { + crc, err := opener() + if err != nil { + return nil, err + } + + if layer.compression == compression.ZStd { + return zstd.ReadCloserLevel(crc, layer.compressionLevel), nil + } + + return ggzip.ReadCloserLevel(crc, layer.compressionLevel), nil + } + } + + for _, opt := range opts { + opt(layer) + } + + // Warn if media type does not match compression + var mediaTypeMismatch = false + switch layer.compression { + case compression.GZip: + mediaTypeMismatch = + layer.mediaType != types.OCILayer && + layer.mediaType != types.OCIRestrictedLayer && + layer.mediaType != types.DockerLayer + + case compression.ZStd: + mediaTypeMismatch = layer.mediaType != types.OCILayerZStd + } + + if mediaTypeMismatch { + logs.Warn.Printf("Unexpected mediaType (%s) for selected compression in %s in LayerFromOpener().", layer.mediaType, layer.compression) + } + + if layer.digest, layer.size, err = computeDigest(layer.compressedopener); err != nil { + return nil, err + } + + empty := v1.Hash{} + if layer.diffID == empty { + if layer.diffID, err = computeDiffID(layer.uncompressedopener); err != nil { + return nil, err + } + } + + return layer, nil +} + +// LayerFromReader returns a v1.Layer given a io.Reader. +// +// The reader's contents are read and buffered to a temp file in the process. +// +// Deprecated: Use LayerFromOpener or stream.NewLayer instead, if possible. +func LayerFromReader(reader io.Reader, opts ...LayerOption) (v1.Layer, error) { + tmp, err := os.CreateTemp("", "") + if err != nil { + return nil, fmt.Errorf("creating temp file to buffer reader: %w", err) + } + if _, err := io.Copy(tmp, reader); err != nil { + return nil, fmt.Errorf("writing temp file to buffer reader: %w", err) + } + return LayerFromFile(tmp.Name(), opts...) +} + +func computeDigest(opener Opener) (v1.Hash, int64, error) { + rc, err := opener() + if err != nil { + return v1.Hash{}, 0, err + } + defer rc.Close() + + return v1.SHA256(rc) +} + +func computeDiffID(opener Opener) (v1.Hash, error) { + rc, err := opener() + if err != nil { + return v1.Hash{}, err + } + defer rc.Close() + + digest, _, err := v1.SHA256(rc) + return digest, err +} diff --git a/pkg/v1/tarball/layer_test.go b/pkg/v1/tarball/layer_test.go new file mode 100644 index 0000000..5d93360 --- /dev/null +++ b/pkg/v1/tarball/layer_test.go @@ -0,0 +1,381 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "bytes" + "compress/gzip" + "io" + "os" + "testing" + + "github.com/containerd/stargz-snapshotter/estargz" + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/compression" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestLayerFromFile(t *testing.T) { + setupFixtures(t) + defer teardownFixtures(t) + + tarLayer, err := LayerFromFile("testdata/content.tar") + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + tarGzLayer, err := LayerFromFile("gzip_content.tgz") + if err != nil { + t.Fatalf("Unable to create layer from compressed tar file: %v", err) + } + + tarZstdLayer, err := LayerFromFile("zstd_content.tar.zst") + if err != nil { + t.Fatalf("Unable to create layer from compressed tar file: %v", err) + } + + if err := compare.Layers(tarLayer, tarGzLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } + + if err := compare.Layers(tarLayer, tarZstdLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } + + if err := validate.Layer(tarLayer); err != nil { + t.Errorf("validate.Layer(tarLayer): %v", err) + } + + if err := validate.Layer(tarGzLayer); err != nil { + t.Errorf("validate.Layer(tarGzLayer): %v", err) + } + + if err := validate.Layer(tarZstdLayer); err != nil { + t.Errorf("validate.Layer(tarZstdLayer): %v", err) + } + + getTestDigest := func(testName string, opts ...LayerOption) v1.Hash { + layer, err := LayerFromFile("testdata/content.tar", opts...) + if err != nil { + t.Fatalf("Unable to create layer with '%s' compression from tar file: %v", testName, err) + } + + digest, err := layer.Digest() + if err != nil { + t.Fatalf("Unable to generate digest with '%s' compression: %v", testName, err) + } + + return digest + } + + defaultDigest := getTestDigest("Gzip Default", WithCompressionLevel(gzip.DefaultCompression)) + speedDigest := getTestDigest("Gzip BestSpeed", WithCompressionLevel(gzip.BestSpeed)) + zstdDigest := getTestDigest("Zstd Default", WithCompression(compression.ZStd)) + zstdDigest1 := getTestDigest("Zstd BestSpeed", WithCompression(compression.ZStd), WithCompressionLevel(1)) + + if defaultDigest.String() == speedDigest.String() { + t.Errorf("expected digests to differ: %s", defaultDigest.String()) + } + + if defaultDigest.String() == zstdDigest.String() { + t.Errorf("expected digests to differ: %s", defaultDigest.String()) + } + + if defaultDigest.String() == zstdDigest1.String() { + t.Errorf("expected digests to differ: %s", defaultDigest.String()) + } +} + +func TestLayerFromFileEstargz(t *testing.T) { + setupFixtures(t) + defer teardownFixtures(t) + + tarLayer, err := LayerFromFile("testdata/content.tar", WithEstargz) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + if err := validate.Layer(tarLayer); err != nil { + t.Errorf("validate.Layer(tarLayer): %v", err) + } + + tarLayerDefaultCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.DefaultCompression)) + if err != nil { + t.Fatalf("Unable to create layer with 'Default' compression from tar file: %v", err) + } + descriptorDefaultCompression, err := tarLayerDefaultCompression.(*layer).Descriptor() + if err != nil { + t.Fatalf("Descriptor() = %v", err) + } else if len(descriptorDefaultCompression.Annotations) != 1 { + t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorDefaultCompression.Annotations) + } + + defaultDigest, err := tarLayerDefaultCompression.Digest() + if err != nil { + t.Fatal("Unable to generate digest with 'Default' compression", err) + } + + tarLayerSpeedCompression, err := LayerFromFile("testdata/content.tar", WithEstargz, WithCompressionLevel(gzip.BestSpeed)) + if err != nil { + t.Fatalf("Unable to create layer with 'BestSpeed' compression from tar file: %v", err) + } + descriptorSpeedCompression, err := tarLayerSpeedCompression.(*layer).Descriptor() + if err != nil { + t.Fatalf("Descriptor() = %v", err) + } else if len(descriptorSpeedCompression.Annotations) != 1 { + t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorSpeedCompression.Annotations) + } + + speedDigest, err := tarLayerSpeedCompression.Digest() + if err != nil { + t.Fatal("Unable to generate digest with 'BestSpeed' compression", err) + } + + if defaultDigest.String() == speedDigest.String() { + t.Errorf("expected digests to differ: %s", defaultDigest.String()) + } + + if descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation] == descriptorSpeedCompression.Annotations[estargz.TOCJSONDigestAnnotation] { + t.Errorf("wanted different toc digests got default: %s, speed: %s", + descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation], + descriptorSpeedCompression.Annotations[estargz.TOCJSONDigestAnnotation]) + } + + tarLayerPrioritizedFiles, err := LayerFromFile("testdata/content.tar", + WithEstargz, + // We compare with default, so pass for apples-to-apples comparison. + WithCompressionLevel(gzip.DefaultCompression), + // By passing a list of priority files, we expect the layer to be different. + WithEstargzOptions(estargz.WithPrioritizedFiles([]string{ + "./bat", + }))) + if err != nil { + t.Fatalf("Unable to create layer with prioritized files from tar file: %v", err) + } + descriptorPrioritizedFiles, err := tarLayerPrioritizedFiles.(*layer).Descriptor() + if err != nil { + t.Fatalf("Descriptor() = %v", err) + } else if len(descriptorPrioritizedFiles.Annotations) != 1 { + t.Errorf("Annotations = %#v, wanted 1 annotation", descriptorPrioritizedFiles.Annotations) + } + + prioritizedDigest, err := tarLayerPrioritizedFiles.Digest() + if err != nil { + t.Fatal("Unable to generate digest with prioritized files", err) + } + + if defaultDigest.String() == prioritizedDigest.String() { + t.Errorf("expected digests to differ: %s", defaultDigest.String()) + } + + if descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation] == descriptorPrioritizedFiles.Annotations[estargz.TOCJSONDigestAnnotation] { + t.Errorf("wanted different toc digests got default: %s, prioritized: %s", + descriptorDefaultCompression.Annotations[estargz.TOCJSONDigestAnnotation], + descriptorPrioritizedFiles.Annotations[estargz.TOCJSONDigestAnnotation]) + } +} + +func TestLayerFromOpenerReader(t *testing.T) { + setupFixtures(t) + defer teardownFixtures(t) + + ucBytes, err := os.ReadFile("testdata/content.tar") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + count := 0 + ucOpener := func() (io.ReadCloser, error) { + count++ + return io.NopCloser(bytes.NewReader(ucBytes)), nil + } + tarLayer, err := LayerFromOpener(ucOpener, WithCompressedCaching) + if err != nil { + t.Fatal("Unable to create layer from tar file:", err) + } + for i := 0; i < 10; i++ { + tarLayer.Compressed() + } + + // Store the count and reset the counter. + cachedCount := count + count = 0 + + tarLayer, err = LayerFromOpener(ucOpener) + if err != nil { + t.Fatal("Unable to create layer from tar file:", err) + } + for i := 0; i < 10; i++ { + tarLayer.Compressed() + } + + // We expect three calls: gzip sniff, diffid computation, cached compression + if cachedCount != 3 { + t.Errorf("cached count = %d, wanted %d", cachedCount, 3) + } + if cachedCount+10 != count { + t.Errorf("count = %d, wanted %d", count, cachedCount+10) + } + + gzBytes, err := os.ReadFile("gzip_content.tgz") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + gzOpener := func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(gzBytes)), nil + } + tarGzLayer, err := LayerFromOpener(gzOpener) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + if err := compare.Layers(tarLayer, tarGzLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } + + zstdBytes, err := os.ReadFile("zstd_content.tar.zst") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + zstdOpener := func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(zstdBytes)), nil + } + tarZstdLayer, err := LayerFromOpener(zstdOpener) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + if err := compare.Layers(tarLayer, tarZstdLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } +} + +func TestWithMediaType(t *testing.T) { + setupFixtures(t) + defer teardownFixtures(t) + + l, err := LayerFromFile("testdata/content.tar") + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + got, err := l.MediaType() + if err != nil { + t.Fatalf("MediaType: %v", err) + } + if want := types.DockerLayer; got != want { + t.Errorf("got %v, want %v", got, want) + } + + l, err = LayerFromFile("testdata/content.tar", WithMediaType(types.OCILayer)) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + got, err = l.MediaType() + if err != nil { + t.Fatalf("MediaType: %v", err) + } + if want := types.OCILayer; got != want { + t.Errorf("got %v, want %v", got, want) + } +} + +func TestLayerFromReader(t *testing.T) { + setupFixtures(t) + defer teardownFixtures(t) + + ucBytes, err := os.ReadFile("testdata/content.tar") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + tarLayer, err := LayerFromReader(bytes.NewReader(ucBytes)) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + gzBytes, err := os.ReadFile("gzip_content.tgz") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + tarGzLayer, err := LayerFromReader(bytes.NewReader(gzBytes)) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + if err := compare.Layers(tarLayer, tarGzLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } + + zstdBytes, err := os.ReadFile("zstd_content.tar.zst") + if err != nil { + t.Fatalf("Unable to read tar file: %v", err) + } + tarZstdLayer, err := LayerFromReader(bytes.NewReader(zstdBytes)) + if err != nil { + t.Fatalf("Unable to create layer from tar file: %v", err) + } + + if err := compare.Layers(tarLayer, tarZstdLayer); err != nil { + t.Errorf("compare.Layers: %v", err) + } +} + +// Compression settings matter in order for the digest, size, +// compressed assertions to pass +// +// Since our gzip.GzipReadCloser uses gzip.BestSpeed +// we need our fixture to use the same - bazel's pkg_tar doesn't +// seem to let you control compression settings +func setupFixtures(t *testing.T) { + t.Helper() + + setupCompressedTar(t, "gzip_content.tgz") + setupCompressedTar(t, "zstd_content.tar.zst") +} + +func setupCompressedTar(t *testing.T, fileName string) { + t.Helper() + in, err := os.Open("testdata/content.tar") + if err != nil { + t.Errorf("Error setting up fixtures: %v", err) + } + + defer in.Close() + + out, err := os.Create(fileName) + if err != nil { + t.Errorf("Error setting up fixtures: %v", err) + } + + defer out.Close() + + gw, _ := gzip.NewWriterLevel(out, gzip.BestSpeed) + defer gw.Close() + + _, err = io.Copy(gw, in) + if err != nil { + t.Errorf("Error setting up fixtures: %v", err) + } +} + +func teardownFixtures(t *testing.T) { + t.Helper() + if err := os.Remove("gzip_content.tgz"); err != nil { + t.Errorf("Error tearing down fixtures: %v", err) + } + if err := os.Remove("zstd_content.tar.zst"); err != nil { + t.Errorf("Error tearing down fixtures: %v", err) + } +} diff --git a/pkg/v1/tarball/progress_test.go b/pkg/v1/tarball/progress_test.go new file mode 100644 index 0000000..c722749 --- /dev/null +++ b/pkg/v1/tarball/progress_test.go @@ -0,0 +1,57 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball_test + +import ( + "errors" + "fmt" + "io" + "os" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" +) + +func ExampleWithProgress() { + // buffered channel to make the example test easier + c := make(chan v1.Update, 200) + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + fmt.Printf("error creating temp file: %v\n", err) + return + } + defer fp.Close() + defer os.Remove(fp.Name()) + + img, err := tarball.ImageFromPath("testdata/test_image_1.tar", nil) + go func() { + _ = tarball.WriteToFile(fp.Name(), nil, img, tarball.WithProgress(c)) + }() + for update := range c { + switch { + case update.Error != nil && errors.Is(update.Error, io.EOF): + fmt.Fprintf(os.Stderr, "receive error message: %v\n", err) + fmt.Printf("%d/%d", update.Complete, update.Total) + // Output: 4096/4096 + return + case update.Error != nil: + fmt.Printf("error writing tarball: %v\n", update.Error) + return + default: + fmt.Fprintf(os.Stderr, "receive update: %#v\n", update) + } + } +} diff --git a/pkg/v1/tarball/testdata/bar b/pkg/v1/tarball/testdata/bar new file mode 100644 index 0000000..5716ca5 --- /dev/null +++ b/pkg/v1/tarball/testdata/bar @@ -0,0 +1 @@ +bar diff --git a/pkg/v1/tarball/testdata/bat/bat b/pkg/v1/tarball/testdata/bat/bat new file mode 100644 index 0000000..1054901 --- /dev/null +++ b/pkg/v1/tarball/testdata/bat/bat @@ -0,0 +1 @@ +bat diff --git a/pkg/v1/tarball/testdata/baz b/pkg/v1/tarball/testdata/baz new file mode 100644 index 0000000..7601807 --- /dev/null +++ b/pkg/v1/tarball/testdata/baz @@ -0,0 +1 @@ +baz diff --git a/pkg/v1/tarball/testdata/content.tar b/pkg/v1/tarball/testdata/content.tar Binary files differnew file mode 100755 index 0000000..55f4d1d --- /dev/null +++ b/pkg/v1/tarball/testdata/content.tar diff --git a/pkg/v1/tarball/testdata/foo b/pkg/v1/tarball/testdata/foo new file mode 100644 index 0000000..257cc56 --- /dev/null +++ b/pkg/v1/tarball/testdata/foo @@ -0,0 +1 @@ +foo diff --git a/pkg/v1/tarball/testdata/no_manifest.tar b/pkg/v1/tarball/testdata/no_manifest.tar Binary files differnew file mode 100644 index 0000000..319db1d --- /dev/null +++ b/pkg/v1/tarball/testdata/no_manifest.tar diff --git a/pkg/v1/tarball/testdata/null_manifest.tar b/pkg/v1/tarball/testdata/null_manifest.tar Binary files differnew file mode 100644 index 0000000..2a65fcd --- /dev/null +++ b/pkg/v1/tarball/testdata/null_manifest.tar diff --git a/pkg/v1/tarball/testdata/test_bundle.tar b/pkg/v1/tarball/testdata/test_bundle.tar Binary files differnew file mode 100755 index 0000000..1ad0f79 --- /dev/null +++ b/pkg/v1/tarball/testdata/test_bundle.tar diff --git a/pkg/v1/tarball/testdata/test_image_1.tar b/pkg/v1/tarball/testdata/test_image_1.tar Binary files differnew file mode 100755 index 0000000..0fe1a21 --- /dev/null +++ b/pkg/v1/tarball/testdata/test_image_1.tar diff --git a/pkg/v1/tarball/testdata/test_image_2.tar b/pkg/v1/tarball/testdata/test_image_2.tar Binary files differnew file mode 100755 index 0000000..bdfe0ef --- /dev/null +++ b/pkg/v1/tarball/testdata/test_image_2.tar diff --git a/pkg/v1/tarball/testdata/test_link.tar b/pkg/v1/tarball/testdata/test_link.tar Binary files differnew file mode 100644 index 0000000..e83064f --- /dev/null +++ b/pkg/v1/tarball/testdata/test_link.tar diff --git a/pkg/v1/tarball/testdata/test_load_manifest.tar b/pkg/v1/tarball/testdata/test_load_manifest.tar Binary files differnew file mode 100644 index 0000000..0fe1a21 --- /dev/null +++ b/pkg/v1/tarball/testdata/test_load_manifest.tar diff --git a/pkg/v1/tarball/write.go b/pkg/v1/tarball/write.go new file mode 100644 index 0000000..e607df1 --- /dev/null +++ b/pkg/v1/tarball/write.go @@ -0,0 +1,457 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" +) + +// WriteToFile writes in the compressed format to a tarball, on disk. +// This is just syntactic sugar wrapping tarball.Write with a new file. +func WriteToFile(p string, ref name.Reference, img v1.Image, opts ...WriteOption) error { + w, err := os.Create(p) + if err != nil { + return err + } + defer w.Close() + + return Write(ref, img, w, opts...) +} + +// MultiWriteToFile writes in the compressed format to a tarball, on disk. +// This is just syntactic sugar wrapping tarball.MultiWrite with a new file. +func MultiWriteToFile(p string, tagToImage map[name.Tag]v1.Image, opts ...WriteOption) error { + refToImage := make(map[name.Reference]v1.Image, len(tagToImage)) + for i, d := range tagToImage { + refToImage[i] = d + } + return MultiRefWriteToFile(p, refToImage, opts...) +} + +// MultiRefWriteToFile writes in the compressed format to a tarball, on disk. +// This is just syntactic sugar wrapping tarball.MultiRefWrite with a new file. +func MultiRefWriteToFile(p string, refToImage map[name.Reference]v1.Image, opts ...WriteOption) error { + w, err := os.Create(p) + if err != nil { + return err + } + defer w.Close() + + return MultiRefWrite(refToImage, w, opts...) +} + +// Write is a wrapper to write a single image and tag to a tarball. +func Write(ref name.Reference, img v1.Image, w io.Writer, opts ...WriteOption) error { + return MultiRefWrite(map[name.Reference]v1.Image{ref: img}, w, opts...) +} + +// MultiWrite writes the contents of each image to the provided writer, in the compressed format. +// The contents are written in the following format: +// One manifest.json file at the top level containing information about several images. +// One file for each layer, named after the layer's SHA. +// One file for the config blob, named after its SHA. +func MultiWrite(tagToImage map[name.Tag]v1.Image, w io.Writer, opts ...WriteOption) error { + refToImage := make(map[name.Reference]v1.Image, len(tagToImage)) + for i, d := range tagToImage { + refToImage[i] = d + } + return MultiRefWrite(refToImage, w, opts...) +} + +// MultiRefWrite writes the contents of each image to the provided writer, in the compressed format. +// The contents are written in the following format: +// One manifest.json file at the top level containing information about several images. +// One file for each layer, named after the layer's SHA. +// One file for the config blob, named after its SHA. +func MultiRefWrite(refToImage map[name.Reference]v1.Image, w io.Writer, opts ...WriteOption) error { + // process options + o := &writeOptions{ + updates: nil, + } + for _, option := range opts { + if err := option(o); err != nil { + return err + } + } + + imageToTags := dedupRefToImage(refToImage) + size, mBytes, err := getSizeAndManifest(imageToTags) + if err != nil { + return sendUpdateReturn(o, err) + } + + return writeImagesToTar(imageToTags, mBytes, size, w, o) +} + +// sendUpdateReturn return the passed in error message, also sending on update channel, if it exists +func sendUpdateReturn(o *writeOptions, err error) error { + if o != nil && o.updates != nil { + o.updates <- v1.Update{ + Error: err, + } + } + return err +} + +// sendProgressWriterReturn return the passed in error message, also sending on update channel, if it exists, along with downloaded information +func sendProgressWriterReturn(pw *progressWriter, err error) error { + if pw != nil { + return pw.Error(err) + } + return err +} + +// writeImagesToTar writes the images to the tarball +func writeImagesToTar(imageToTags map[v1.Image][]string, m []byte, size int64, w io.Writer, o *writeOptions) (err error) { + if w == nil { + return sendUpdateReturn(o, errors.New("must pass valid writer")) + } + + tw := w + var pw *progressWriter + + // we only calculate the sizes and use a progressWriter if we were provided + // an option with a progress channel + if o != nil && o.updates != nil { + pw = &progressWriter{ + w: w, + updates: o.updates, + size: size, + } + tw = pw + } + + tf := tar.NewWriter(tw) + defer tf.Close() + + seenLayerDigests := make(map[string]struct{}) + + for img := range imageToTags { + // Write the config. + cfgName, err := img.ConfigName() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + cfgBlob, err := img.RawConfigFile() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + if err := writeTarEntry(tf, cfgName.String(), bytes.NewReader(cfgBlob), int64(len(cfgBlob))); err != nil { + return sendProgressWriterReturn(pw, err) + } + + // Write the layers. + layers, err := img.Layers() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + layerFiles := make([]string, len(layers)) + for i, l := range layers { + d, err := l.Digest() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + // Munge the file name to appease ancient technology. + // + // tar assumes anything with a colon is a remote tape drive: + // https://www.gnu.org/software/tar/manual/html_section/tar_45.html + // Drop the algorithm prefix, e.g. "sha256:" + hex := d.Hex + + // gunzip expects certain file extensions: + // https://www.gnu.org/software/gzip/manual/html_node/Overview.html + layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex) + + if _, ok := seenLayerDigests[hex]; ok { + continue + } + seenLayerDigests[hex] = struct{}{} + + r, err := l.Compressed() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + blobSize, err := l.Size() + if err != nil { + return sendProgressWriterReturn(pw, err) + } + + if err := writeTarEntry(tf, layerFiles[i], r, blobSize); err != nil { + return sendProgressWriterReturn(pw, err) + } + } + } + if err := writeTarEntry(tf, "manifest.json", bytes.NewReader(m), int64(len(m))); err != nil { + return sendProgressWriterReturn(pw, err) + } + + // be sure to close the tar writer so everything is flushed out before we send our EOF + if err := tf.Close(); err != nil { + return sendProgressWriterReturn(pw, err) + } + // send an EOF to indicate finished on the channel, but nil as our return error + _ = sendProgressWriterReturn(pw, io.EOF) + return nil +} + +// calculateManifest calculates the manifest and optionally the size of the tar file +func calculateManifest(imageToTags map[v1.Image][]string) (m Manifest, err error) { + if len(imageToTags) == 0 { + return nil, errors.New("set of images is empty") + } + + for img, tags := range imageToTags { + cfgName, err := img.ConfigName() + if err != nil { + return nil, err + } + + // Store foreign layer info. + layerSources := make(map[v1.Hash]v1.Descriptor) + + // Write the layers. + layers, err := img.Layers() + if err != nil { + return nil, err + } + layerFiles := make([]string, len(layers)) + for i, l := range layers { + d, err := l.Digest() + if err != nil { + return nil, err + } + // Munge the file name to appease ancient technology. + // + // tar assumes anything with a colon is a remote tape drive: + // https://www.gnu.org/software/tar/manual/html_section/tar_45.html + // Drop the algorithm prefix, e.g. "sha256:" + hex := d.Hex + + // gunzip expects certain file extensions: + // https://www.gnu.org/software/gzip/manual/html_node/Overview.html + layerFiles[i] = fmt.Sprintf("%s.tar.gz", hex) + + // Add to LayerSources if it's a foreign layer. + desc, err := partial.BlobDescriptor(img, d) + if err != nil { + return nil, err + } + if !desc.MediaType.IsDistributable() { + diffid, err := partial.BlobToDiffID(img, d) + if err != nil { + return nil, err + } + layerSources[diffid] = *desc + } + } + + // Generate the tar descriptor and write it. + m = append(m, Descriptor{ + Config: cfgName.String(), + RepoTags: tags, + Layers: layerFiles, + LayerSources: layerSources, + }) + } + // sort by name of the repotags so it is consistent. Alternatively, we could sort by hash of the + // descriptor, but that would make it hard for humans to process + sort.Slice(m, func(i, j int) bool { + return strings.Join(m[i].RepoTags, ",") < strings.Join(m[j].RepoTags, ",") + }) + + return m, nil +} + +// CalculateSize calculates the expected complete size of the output tar file +func CalculateSize(refToImage map[name.Reference]v1.Image) (size int64, err error) { + imageToTags := dedupRefToImage(refToImage) + size, _, err = getSizeAndManifest(imageToTags) + return size, err +} + +func getSizeAndManifest(imageToTags map[v1.Image][]string) (int64, []byte, error) { + m, err := calculateManifest(imageToTags) + if err != nil { + return 0, nil, fmt.Errorf("unable to calculate manifest: %w", err) + } + mBytes, err := json.Marshal(m) + if err != nil { + return 0, nil, fmt.Errorf("could not marshall manifest to bytes: %w", err) + } + + size, err := calculateTarballSize(imageToTags, mBytes) + if err != nil { + return 0, nil, fmt.Errorf("error calculating tarball size: %w", err) + } + return size, mBytes, nil +} + +// calculateTarballSize calculates the size of the tar file +func calculateTarballSize(imageToTags map[v1.Image][]string, mBytes []byte) (size int64, err error) { + seenLayerDigests := make(map[string]struct{}) + for img, name := range imageToTags { + manifest, err := img.Manifest() + if err != nil { + return size, fmt.Errorf("unable to get manifest for img %s: %w", name, err) + } + size += calculateSingleFileInTarSize(manifest.Config.Size) + for _, l := range manifest.Layers { + hex := l.Digest.Hex + if _, ok := seenLayerDigests[hex]; ok { + continue + } + seenLayerDigests[hex] = struct{}{} + size += calculateSingleFileInTarSize(l.Size) + } + } + // add the manifest + size += calculateSingleFileInTarSize(int64(len(mBytes))) + + // add the two padding blocks that indicate end of a tar file + size += 1024 + return size, nil +} + +func dedupRefToImage(refToImage map[name.Reference]v1.Image) map[v1.Image][]string { + imageToTags := make(map[v1.Image][]string) + + for ref, img := range refToImage { + if tag, ok := ref.(name.Tag); ok { + if tags, ok := imageToTags[img]; !ok || tags == nil { + imageToTags[img] = []string{} + } + // Docker cannot load tarballs without an explicit tag: + // https://github.com/google/go-containerregistry/issues/890 + // + // We can't use the fully qualified tag.Name() because of rules_docker: + // https://github.com/google/go-containerregistry/issues/527 + // + // If the tag is "latest", but tag.String() doesn't end in ":latest", + // just append it. Kind of gross, but should work for now. + ts := tag.String() + if tag.Identifier() == name.DefaultTag && !strings.HasSuffix(ts, ":"+name.DefaultTag) { + ts = fmt.Sprintf("%s:%s", ts, name.DefaultTag) + } + imageToTags[img] = append(imageToTags[img], ts) + } else if _, ok := imageToTags[img]; !ok { + imageToTags[img] = nil + } + } + + return imageToTags +} + +// writeTarEntry writes a file to the provided writer with a corresponding tar header +func writeTarEntry(tf *tar.Writer, path string, r io.Reader, size int64) error { + hdr := &tar.Header{ + Mode: 0644, + Typeflag: tar.TypeReg, + Size: size, + Name: path, + } + if err := tf.WriteHeader(hdr); err != nil { + return err + } + _, err := io.Copy(tf, r) + return err +} + +// ComputeManifest get the manifest.json that will be written to the tarball +// for multiple references +func ComputeManifest(refToImage map[name.Reference]v1.Image) (Manifest, error) { + imageToTags := dedupRefToImage(refToImage) + return calculateManifest(imageToTags) +} + +// WriteOption a function option to pass to Write() +type WriteOption func(*writeOptions) error +type writeOptions struct { + updates chan<- v1.Update +} + +// WithProgress create a WriteOption for passing to Write() that enables +// a channel to receive updates as they are downloaded and written to disk. +func WithProgress(updates chan<- v1.Update) WriteOption { + return func(o *writeOptions) error { + o.updates = updates + return nil + } +} + +// progressWriter is a writer which will send the download progress +type progressWriter struct { + w io.Writer + updates chan<- v1.Update + size, complete int64 +} + +func (pw *progressWriter) Write(p []byte) (int, error) { + n, err := pw.w.Write(p) + if err != nil { + return n, err + } + + pw.complete += int64(n) + + pw.updates <- v1.Update{ + Total: pw.size, + Complete: pw.complete, + } + + return n, err +} + +func (pw *progressWriter) Error(err error) error { + pw.updates <- v1.Update{ + Total: pw.size, + Complete: pw.complete, + Error: err, + } + return err +} + +func (pw *progressWriter) Close() error { + pw.updates <- v1.Update{ + Total: pw.size, + Complete: pw.complete, + Error: io.EOF, + } + return io.EOF +} + +// calculateSingleFileInTarSize calculate the size a file will take up in a tar archive, +// given the input data. Provided by rounding up to nearest whole block (512) +// and adding header 512 +func calculateSingleFileInTarSize(in int64) (out int64) { + // doing this manually, because math.Round() works with float64 + out += in + if remainder := out % 512; remainder != 0 { + out += (512 - remainder) + } + out += 512 + return out +} diff --git a/pkg/v1/tarball/write_test.go b/pkg/v1/tarball/write_test.go new file mode 100644 index 0000000..fdfe499 --- /dev/null +++ b/pkg/v1/tarball/write_test.go @@ -0,0 +1,502 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tarball_test + +import ( + "archive/tar" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "testing" + + "github.com/google/go-containerregistry/internal/compare" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/google/go-containerregistry/pkg/v1/validate" +) + +func TestWrite(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image.") + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag.") + } + if err := tarball.WriteToFile(fp.Name(), tag, randImage); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + + // Make sure the image is valid and can be loaded. + // Load it both by nil and by its name. + for _, it := range []*name.Tag{nil, &tag} { + tarImage, err := tarball.ImageFromPath(fp.Name(), it) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + + if err := compare.Images(randImage, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } + + // Try loading a different tag, it should error. + fakeTag, err := name.NewTag("gcr.io/notthistag:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error generating tag: %v", err) + } + if _, err := tarball.ImageFromPath(fp.Name(), &fakeTag); err == nil { + t.Errorf("Expected error loading tag %v from image", fakeTag) + } +} + +func TestMultiWriteSameImage(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image.") + } + + // Make two tags that point to the random image above. + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1.") + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2.") + } + dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test dig3.") + } + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage + refToImage[tag2] = randImage + refToImage[dig3] = randImage + + // Write the images with both tags to the tarball + if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + + if err := compare.Images(randImage, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } +} + +func TestMultiWriteDifferentImages(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage1, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 1.") + } + + // Make another random image + randImage2, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 2.") + } + + // Make another random image + randImage3, err := random.Image(256, 8) + if err != nil { + t.Fatalf("Error creating random image 3.") + } + + // Create two tags, one pointing to each image created. + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1.") + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2.") + } + dig3, err := name.NewDigest("gcr.io/baz/baz@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test dig3.") + } + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage1 + refToImage[tag2] = randImage2 + refToImage[dig3] = randImage3 + + // Write both images to the tarball. + if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref, img := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + + if err := compare.Images(img, tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } +} + +func TestWriteForeignLayers(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 1) + if err != nil { + t.Fatalf("Error creating random image.") + } + tag, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag.") + } + randLayer, err := random.Layer(512, types.DockerForeignLayer) + if err != nil { + t.Fatalf("random.Layer: %v", err) + } + img, err := mutate.Append(randImage, mutate.Addendum{ + Layer: randLayer, + URLs: []string{ + "example.com", + }, + }) + if err != nil { + t.Fatal(err) + } + if err := tarball.WriteToFile(fp.Name(), tag, img); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Fatalf("validate.Image(): %v", err) + } + + m, err := tarImage.Manifest() + if err != nil { + t.Fatal(err) + } + + if got, want := m.Layers[1].MediaType, types.DockerForeignLayer; got != want { + t.Errorf("Wrong MediaType: %s != %s", got, want) + } + if got, want := m.Layers[1].URLs[0], "example.com"; got != want { + t.Errorf("Wrong URLs: %s != %s", got, want) + } +} + +func TestWriteSharedLayers(t *testing.T) { + // Make a tempfile for tarball writes. + fp, err := os.CreateTemp("", "") + if err != nil { + t.Fatalf("Error creating temp file.") + } + t.Log(fp.Name()) + defer fp.Close() + defer os.Remove(fp.Name()) + + // Make a random image + randImage, err := random.Image(256, 1) + if err != nil { + t.Fatalf("Error creating random image.") + } + tag1, err := name.NewTag("gcr.io/foo/bar:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag1.") + } + tag2, err := name.NewTag("gcr.io/baz/bat:latest", name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2.") + } + randLayer, err := random.Layer(512, types.DockerLayer) + if err != nil { + t.Fatalf("random.Layer: %v", err) + } + mutatedImage, err := mutate.Append(randImage, mutate.Addendum{ + Layer: randLayer, + }) + if err != nil { + t.Fatal(err) + } + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage + refToImage[tag2] = mutatedImage + + // Write the images with both tags to the tarball + if err := tarball.MultiRefWriteToFile(fp.Name(), refToImage); err != nil { + t.Fatalf("Unexpected error writing tarball: %v", err) + } + for ref := range refToImage { + tag, ok := ref.(name.Tag) + if !ok { + continue + } + + tarImage, err := tarball.ImageFromPath(fp.Name(), &tag) + if err != nil { + t.Fatalf("Unexpected error reading tarball: %v", err) + } + + if err := validate.Image(tarImage); err != nil { + t.Errorf("validate.Image: %v", err) + } + + if err := compare.Images(refToImage[tag], tarImage); err != nil { + t.Errorf("compare.Images: %v", err) + } + } + _, err = fp.Seek(0, io.SeekStart) + if err != nil { + t.Fatalf("Seek to start of file: %v", err) + } + layers, err := randImage.Layers() + if err != nil { + t.Fatalf("Get image layers: %v", err) + } + layers = append(layers, randLayer) + wantDigests := make(map[string]struct{}) + for _, layer := range layers { + d, err := layer.Digest() + if err != nil { + t.Fatalf("Get layer digest: %v", err) + } + wantDigests[d.Hex] = struct{}{} + } + + const layerFileSuffix = ".tar.gz" + r := tar.NewReader(fp) + for { + hdr, err := r.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + t.Fatalf("Get tar header: %v", err) + } + if strings.HasSuffix(hdr.Name, layerFileSuffix) { + hex := hdr.Name[:len(hdr.Name)-len(layerFileSuffix)] + if _, ok := wantDigests[hex]; ok { + delete(wantDigests, hex) + } else { + t.Errorf("Found unwanted layer with digest %q", hex) + } + } + } + if len(wantDigests) != 0 { + for hex := range wantDigests { + t.Errorf("Expected to find layer with digest %q but it didn't exist", hex) + } + } +} + +func TestComputeManifest(t *testing.T) { + var randomTag, mutatedTag = "ubuntu", "gcr.io/baz/bat:latest" + + // https://github.com/google/go-containerregistry/issues/890 + randomTagWritten := "ubuntu:latest" + + // Make a random image + randImage, err := random.Image(256, 1) + if err != nil { + t.Fatalf("Error creating random image.") + } + randConfig, err := randImage.ConfigName() + if err != nil { + t.Fatalf("error getting random image config: %v", err) + } + tag1, err := name.NewTag(randomTag) + if err != nil { + t.Fatalf("Error creating test tag1.") + } + tag2, err := name.NewTag(mutatedTag, name.StrictValidation) + if err != nil { + t.Fatalf("Error creating test tag2.") + } + randLayer, err := random.Layer(512, types.DockerLayer) + if err != nil { + t.Fatalf("random.Layer: %v", err) + } + mutatedImage, err := mutate.Append(randImage, mutate.Addendum{ + Layer: randLayer, + }) + if err != nil { + t.Fatal(err) + } + mutatedConfig, err := mutatedImage.ConfigName() + if err != nil { + t.Fatalf("error getting mutated image config: %v", err) + } + randomLayersHashes, err := getLayersHashes(randImage) + if err != nil { + t.Fatalf("error getting random image hashes: %v", err) + } + randomLayersFilenames := getLayersFilenames(randomLayersHashes) + + mutatedLayersHashes, err := getLayersHashes(mutatedImage) + if err != nil { + t.Fatalf("error getting mutated image hashes: %v", err) + } + mutatedLayersFilenames := getLayersFilenames(mutatedLayersHashes) + + refToImage := make(map[name.Reference]v1.Image) + refToImage[tag1] = randImage + refToImage[tag2] = mutatedImage + + // calculate the manifest + m, err := tarball.ComputeManifest(refToImage) + if err != nil { + t.Fatalf("Unexpected error calculating manifest: %v", err) + } + // the order of these two is based on the repo tags + // so mutated "gcr.io/baz/bat:latest" is before random "gcr.io/foo/bar:latest" + expected := []tarball.Descriptor{ + { + Config: mutatedConfig.String(), + RepoTags: []string{mutatedTag}, + Layers: mutatedLayersFilenames, + }, + { + Config: randConfig.String(), + RepoTags: []string{randomTagWritten}, + Layers: randomLayersFilenames, + }, + } + if len(m) != len(expected) { + t.Fatalf("mismatched manifest lengths: actual %d, expected %d", len(m), len(expected)) + } + mBytes, err := json.Marshal(m) + if err != nil { + t.Fatalf("unable to marshall actual manifest to json: %v", err) + } + eBytes, err := json.Marshal(expected) + if err != nil { + t.Fatalf("unable to marshall expected manifest to json: %v", err) + } + if !bytes.Equal(mBytes, eBytes) { + t.Errorf("mismatched manifests.\nActual: %s\nExpected: %s", string(mBytes), string(eBytes)) + } +} + +func TestComputeManifest_FailsOnNoRefs(t *testing.T) { + _, err := tarball.ComputeManifest(nil) + if err == nil || !strings.Contains(err.Error(), "set of images is empty") { + t.Error("expected calculateManifest to fail with nil input") + } + + _, err = tarball.ComputeManifest(map[name.Reference]v1.Image{}) + if err == nil || !strings.Contains(err.Error(), "set of images is empty") { + t.Error("expected calculateManifest to fail with empty input") + } +} + +func getLayersHashes(img v1.Image) ([]string, error) { + hashes := []string{} + layers, err := img.Layers() + if err != nil { + return nil, fmt.Errorf("error getting image layers: %w", err) + } + for i, l := range layers { + hash, err := l.Digest() + if err != nil { + return nil, fmt.Errorf("error getting digest for layer %d: %w", i, err) + } + hashes = append(hashes, hash.Hex) + } + return hashes, nil +} + +func getLayersFilenames(hashes []string) []string { + filenames := []string{} + for _, h := range hashes { + filenames = append(filenames, fmt.Sprintf("%s.tar.gz", h)) + } + return filenames +} diff --git a/pkg/v1/types/types.go b/pkg/v1/types/types.go new file mode 100644 index 0000000..efc6bd6 --- /dev/null +++ b/pkg/v1/types/types.go @@ -0,0 +1,82 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package types holds common OCI media types. +package types + +// MediaType is an enumeration of the supported mime types that an element of an image might have. +type MediaType string + +// The collection of known MediaType values. +const ( + OCIContentDescriptor MediaType = "application/vnd.oci.descriptor.v1+json" + OCIImageIndex MediaType = "application/vnd.oci.image.index.v1+json" + OCIManifestSchema1 MediaType = "application/vnd.oci.image.manifest.v1+json" + OCIConfigJSON MediaType = "application/vnd.oci.image.config.v1+json" + OCILayer MediaType = "application/vnd.oci.image.layer.v1.tar+gzip" + OCILayerZStd MediaType = "application/vnd.oci.image.layer.v1.tar+zstd" + OCIRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip" + OCIUncompressedLayer MediaType = "application/vnd.oci.image.layer.v1.tar" + OCIUncompressedRestrictedLayer MediaType = "application/vnd.oci.image.layer.nondistributable.v1.tar" + + DockerManifestSchema1 MediaType = "application/vnd.docker.distribution.manifest.v1+json" + DockerManifestSchema1Signed MediaType = "application/vnd.docker.distribution.manifest.v1+prettyjws" + DockerManifestSchema2 MediaType = "application/vnd.docker.distribution.manifest.v2+json" + DockerManifestList MediaType = "application/vnd.docker.distribution.manifest.list.v2+json" + DockerLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip" + DockerConfigJSON MediaType = "application/vnd.docker.container.image.v1+json" + DockerPluginConfig MediaType = "application/vnd.docker.plugin.v1+json" + DockerForeignLayer MediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" + DockerUncompressedLayer MediaType = "application/vnd.docker.image.rootfs.diff.tar" + + OCIVendorPrefix = "vnd.oci" + DockerVendorPrefix = "vnd.docker" +) + +// IsDistributable returns true if a layer is distributable, see: +// https://github.com/opencontainers/image-spec/blob/master/layer.md#non-distributable-layers +func (m MediaType) IsDistributable() bool { + switch m { + case DockerForeignLayer, OCIRestrictedLayer, OCIUncompressedRestrictedLayer: + return false + } + return true +} + +// IsImage returns true if the mediaType represents an image manifest, as opposed to something else, like an index. +func (m MediaType) IsImage() bool { + switch m { + case OCIManifestSchema1, DockerManifestSchema2: + return true + } + return false +} + +// IsIndex returns true if the mediaType represents an index, as opposed to something else, like an image. +func (m MediaType) IsIndex() bool { + switch m { + case OCIImageIndex, DockerManifestList: + return true + } + return false +} + +// IsConfig returns true if the mediaType represents a config, as opposed to something else, like an image. +func (m MediaType) IsConfig() bool { + switch m { + case OCIConfigJSON, DockerConfigJSON: + return true + } + return false +} diff --git a/pkg/v1/types/types_test.go b/pkg/v1/types/types_test.go new file mode 100644 index 0000000..7a8d356 --- /dev/null +++ b/pkg/v1/types/types_test.go @@ -0,0 +1,112 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import "testing" + +func TestIsDistributable(t *testing.T) { + for _, mt := range []MediaType{ + OCIRestrictedLayer, + OCIUncompressedRestrictedLayer, + DockerForeignLayer, + } { + if mt.IsDistributable() { + t.Errorf("%s: should not be distributable", mt) + } + } + + for _, mt := range []MediaType{ + OCIContentDescriptor, + OCIImageIndex, + OCIManifestSchema1, + OCIConfigJSON, + OCILayer, + OCIUncompressedLayer, + DockerManifestSchema1, + DockerManifestSchema1Signed, + DockerManifestSchema2, + DockerManifestList, + DockerLayer, + DockerConfigJSON, + DockerPluginConfig, + DockerUncompressedLayer, + } { + if !mt.IsDistributable() { + t.Errorf("%s: should be distributable", mt) + } + } +} + +func TestIsImage(t *testing.T) { + for _, mt := range []MediaType{ + OCIManifestSchema1, DockerManifestSchema2, + } { + if !mt.IsImage() { + t.Errorf("%s: should be image", mt) + } + } + + for _, mt := range []MediaType{ + OCIContentDescriptor, + OCIImageIndex, + OCIConfigJSON, + OCILayer, + OCIRestrictedLayer, + OCIUncompressedLayer, + OCIUncompressedRestrictedLayer, + + DockerManifestList, + DockerLayer, + DockerConfigJSON, + DockerPluginConfig, + DockerForeignLayer, + DockerUncompressedLayer, + } { + if mt.IsImage() { + t.Errorf("%s: should not be image", mt) + } + } +} + +func TestIsIndex(t *testing.T) { + for _, mt := range []MediaType{ + OCIImageIndex, DockerManifestList, + } { + if !mt.IsIndex() { + t.Errorf("%s: should be index", mt) + } + } + + for _, mt := range []MediaType{ + OCIContentDescriptor, + OCIConfigJSON, + OCILayer, + OCIRestrictedLayer, + OCIUncompressedLayer, + OCIUncompressedRestrictedLayer, + OCIManifestSchema1, + + DockerManifestSchema2, + DockerLayer, + DockerConfigJSON, + DockerPluginConfig, + DockerForeignLayer, + DockerUncompressedLayer, + } { + if mt.IsIndex() { + t.Errorf("%s: should not be index", mt) + } + } +} diff --git a/pkg/v1/validate/doc.go b/pkg/v1/validate/doc.go new file mode 100644 index 0000000..91ca87a --- /dev/null +++ b/pkg/v1/validate/doc.go @@ -0,0 +1,16 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package validate provides methods for validating image correctness. +package validate diff --git a/pkg/v1/validate/image.go b/pkg/v1/validate/image.go new file mode 100644 index 0000000..94fb767 --- /dev/null +++ b/pkg/v1/validate/image.go @@ -0,0 +1,288 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + + "github.com/google/go-cmp/cmp" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" +) + +// Image validates that img does not violate any invariants of the image format. +func Image(img v1.Image, opt ...Option) error { + errs := []string{} + if err := validateLayers(img, opt...); err != nil { + errs = append(errs, fmt.Sprintf("validating layers: %v", err)) + } + + if err := validateConfig(img); err != nil { + errs = append(errs, fmt.Sprintf("validating config: %v", err)) + } + + if err := validateManifest(img); err != nil { + errs = append(errs, fmt.Sprintf("validating manifest: %v", err)) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n\n")) + } + return nil +} + +func validateConfig(img v1.Image) error { + cn, err := img.ConfigName() + if err != nil { + return err + } + + rc, err := img.RawConfigFile() + if err != nil { + return err + } + + hash, size, err := v1.SHA256(bytes.NewReader(rc)) + if err != nil { + return err + } + + m, err := img.Manifest() + if err != nil { + return err + } + + cf, err := img.ConfigFile() + if err != nil { + return err + } + + pcf, err := v1.ParseConfigFile(bytes.NewReader(rc)) + if err != nil { + return err + } + + errs := []string{} + if cn != hash { + errs = append(errs, fmt.Sprintf("mismatched config digest: ConfigName()=%s, SHA256(RawConfigFile())=%s", cn, hash)) + } + + if want, got := m.Config.Size, size; want != got { + errs = append(errs, fmt.Sprintf("mismatched config size: Manifest.Config.Size()=%d, len(RawConfigFile())=%d", want, got)) + } + + if diff := cmp.Diff(pcf, cf); diff != "" { + errs = append(errs, fmt.Sprintf("mismatched config content: (-ParseConfigFile(RawConfigFile()) +ConfigFile()) %s", diff)) + } + + if cf.RootFS.Type != "layers" { + errs = append(errs, fmt.Sprintf("invalid ConfigFile.RootFS.Type: %q != %q", cf.RootFS.Type, "layers")) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +func validateLayers(img v1.Image, opt ...Option) error { + o := makeOptions(opt...) + + layers, err := img.Layers() + if err != nil { + return err + } + + if o.fast { + return layersExist(layers) + } + + digests := []v1.Hash{} + diffids := []v1.Hash{} + udiffids := []v1.Hash{} + sizes := []int64{} + for i, layer := range layers { + cl, err := computeLayer(layer) + if errors.Is(err, io.ErrUnexpectedEOF) { + // Errored while reading tar content of layer because a header or + // content section was not the correct length. This is most likely + // due to an incomplete download or otherwise interrupted process. + m, err := img.Manifest() + if err != nil { + return fmt.Errorf("undersized layer[%d] content", i) + } + return fmt.Errorf("undersized layer[%d] content: Manifest.Layers[%d].Size=%d", i, i, m.Layers[i].Size) + } + if err != nil { + return err + } + // Compute all of these first before we call Config() and Manifest() to allow + // for lazy access e.g. for stream.Layer. + digests = append(digests, cl.digest) + diffids = append(diffids, cl.diffid) + udiffids = append(udiffids, cl.uncompressedDiffid) + sizes = append(sizes, cl.size) + } + + cf, err := img.ConfigFile() + if err != nil { + return err + } + + m, err := img.Manifest() + if err != nil { + return err + } + + errs := []string{} + for i, layer := range layers { + digest, err := layer.Digest() + if err != nil { + return err + } + diffid, err := layer.DiffID() + if err != nil { + return err + } + size, err := layer.Size() + if err != nil { + return err + } + mediaType, err := layer.MediaType() + if err != nil { + return err + } + + if _, err := img.LayerByDigest(digest); err != nil { + return err + } + + if _, err := img.LayerByDiffID(diffid); err != nil { + return err + } + + if digest != digests[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] digest: Digest()=%s, SHA256(Compressed())=%s", i, digest, digests[i])) + } + + if m.Layers[i].Digest != digests[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] digest: Manifest.Layers[%d].Digest=%s, SHA256(Compressed())=%s", i, i, m.Layers[i].Digest, digests[i])) + } + + if diffid != diffids[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", i, diffid, diffids[i])) + } + + if diffid != udiffids[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: DiffID()=%s, SHA256(Uncompressed())=%s", i, diffid, udiffids[i])) + } + + if cf.RootFS.DiffIDs[i] != diffids[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] diffid: ConfigFile.RootFS.DiffIDs[%d]=%s, SHA256(Gunzip(Compressed()))=%s", i, i, cf.RootFS.DiffIDs[i], diffids[i])) + } + + if size != sizes[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] size: Size()=%d, len(Compressed())=%d", i, size, sizes[i])) + } + + if m.Layers[i].Size != sizes[i] { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] size: Manifest.Layers[%d].Size=%d, len(Compressed())=%d", i, i, m.Layers[i].Size, sizes[i])) + } + + if m.Layers[i].MediaType != mediaType { + errs = append(errs, fmt.Sprintf("mismatched layer[%d] mediaType: Manifest.Layers[%d].MediaType=%s, layer.MediaType()=%s", i, i, m.Layers[i].MediaType, mediaType)) + } + } + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +func validateManifest(img v1.Image) error { + digest, err := img.Digest() + if err != nil { + return err + } + + size, err := img.Size() + if err != nil { + return err + } + + rm, err := img.RawManifest() + if err != nil { + return err + } + + hash, _, err := v1.SHA256(bytes.NewReader(rm)) + if err != nil { + return err + } + + m, err := img.Manifest() + if err != nil { + return err + } + + pm, err := v1.ParseManifest(bytes.NewReader(rm)) + if err != nil { + return err + } + + errs := []string{} + if digest != hash { + errs = append(errs, fmt.Sprintf("mismatched manifest digest: Digest()=%s, SHA256(RawManifest())=%s", digest, hash)) + } + + if diff := cmp.Diff(pm, m); diff != "" { + errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseManifest(RawManifest()) +Manifest()) %s", diff)) + } + + if size != int64(len(rm)) { + errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm))) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +func layersExist(layers []v1.Layer) error { + errs := []string{} + for _, layer := range layers { + ok, err := partial.Exists(layer) + if err != nil { + errs = append(errs, err.Error()) + } + if !ok { + errs = append(errs, "layer does not exist") + } + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} diff --git a/pkg/v1/validate/index.go b/pkg/v1/validate/index.go new file mode 100644 index 0000000..7514dc4 --- /dev/null +++ b/pkg/v1/validate/index.go @@ -0,0 +1,175 @@ +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +import ( + "bytes" + "errors" + "fmt" + "strings" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-containerregistry/pkg/logs" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +// Index validates that idx does not violate any invariants of the index format. +func Index(idx v1.ImageIndex, opt ...Option) error { + errs := []string{} + + if err := validateChildren(idx, opt...); err != nil { + errs = append(errs, fmt.Sprintf("validating children: %v", err)) + } + + if err := validateIndexManifest(idx); err != nil { + errs = append(errs, fmt.Sprintf("validating index manifest: %v", err)) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n\n")) + } + return nil +} + +type withLayer interface { + Layer(v1.Hash) (v1.Layer, error) +} + +func validateChildren(idx v1.ImageIndex, opt ...Option) error { + manifest, err := idx.IndexManifest() + if err != nil { + return err + } + + errs := []string{} + for i, desc := range manifest.Manifests { + switch desc.MediaType { + case types.OCIImageIndex, types.DockerManifestList: + idx, err := idx.ImageIndex(desc.Digest) + if err != nil { + return err + } + if err := Index(idx, opt...); err != nil { + errs = append(errs, fmt.Sprintf("failed to validate index Manifests[%d](%s): %v", i, desc.Digest, err)) + } + if err := validateMediaType(idx, desc.MediaType); err != nil { + errs = append(errs, fmt.Sprintf("failed to validate index MediaType[%d](%s): %v", i, desc.Digest, err)) + } + case types.OCIManifestSchema1, types.DockerManifestSchema2: + img, err := idx.Image(desc.Digest) + if err != nil { + return err + } + if err := Image(img, opt...); err != nil { + errs = append(errs, fmt.Sprintf("failed to validate image Manifests[%d](%s): %v", i, desc.Digest, err)) + } + if err := validateMediaType(img, desc.MediaType); err != nil { + errs = append(errs, fmt.Sprintf("failed to validate image MediaType[%d](%s): %v", i, desc.Digest, err)) + } + default: + // Workaround for #819. + if wl, ok := idx.(withLayer); ok { + layer, err := wl.Layer(desc.Digest) + if err != nil { + return fmt.Errorf("failed to get layer Manifests[%d]: %w", i, err) + } + if err := Layer(layer, opt...); err != nil { + lerr := fmt.Sprintf("failed to validate layer Manifests[%d](%s): %v", i, desc.Digest, err) + if desc.MediaType.IsDistributable() { + errs = append(errs, lerr) + } else { + logs.Warn.Printf("nondistributable layer failure: %v", lerr) + } + } + } else { + logs.Warn.Printf("Unexpected manifest: %s", desc.MediaType) + } + } + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +type withMediaType interface { + MediaType() (types.MediaType, error) +} + +func validateMediaType(i withMediaType, want types.MediaType) error { + got, err := i.MediaType() + if err != nil { + return err + } + if want != got { + return fmt.Errorf("mismatched mediaType: MediaType() = %v != %v", got, want) + } + + return nil +} + +func validateIndexManifest(idx v1.ImageIndex) error { + digest, err := idx.Digest() + if err != nil { + return err + } + + size, err := idx.Size() + if err != nil { + return err + } + + rm, err := idx.RawManifest() + if err != nil { + return err + } + + hash, _, err := v1.SHA256(bytes.NewReader(rm)) + if err != nil { + return err + } + + m, err := idx.IndexManifest() + if err != nil { + return err + } + + pm, err := v1.ParseIndexManifest(bytes.NewReader(rm)) + if err != nil { + return err + } + + errs := []string{} + if digest != hash { + errs = append(errs, fmt.Sprintf("mismatched manifest digest: Digest()=%s, SHA256(RawManifest())=%s", digest, hash)) + } + + if diff := cmp.Diff(pm, m); diff != "" { + errs = append(errs, fmt.Sprintf("mismatched manifest content: (-ParseIndexManifest(RawManifest()) +Manifest()) %s", diff)) + } + + if size != int64(len(rm)) { + errs = append(errs, fmt.Sprintf("mismatched manifest size: Size()=%d, len(RawManifest())=%d", size, len(rm))) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} diff --git a/pkg/v1/validate/layer.go b/pkg/v1/validate/layer.go new file mode 100644 index 0000000..fdd8f38 --- /dev/null +++ b/pkg/v1/validate/layer.go @@ -0,0 +1,191 @@ +// Copyright 2019 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +import ( + "archive/tar" + "compress/gzip" + "crypto" + "encoding/hex" + "errors" + "fmt" + "io" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/partial" +) + +// Layer validates that the values return by its methods are consistent with the +// contents returned by Compressed and Uncompressed. +func Layer(layer v1.Layer, opt ...Option) error { + o := makeOptions(opt...) + if o.fast { + ok, err := partial.Exists(layer) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("layer does not exist") + } + return nil + } + + cl, err := computeLayer(layer) + if err != nil { + return err + } + + errs := []string{} + + digest, err := layer.Digest() + if err != nil { + return err + } + diffid, err := layer.DiffID() + if err != nil { + return err + } + size, err := layer.Size() + if err != nil { + return err + } + + if digest != cl.digest { + errs = append(errs, fmt.Sprintf("mismatched digest: Digest()=%s, SHA256(Compressed())=%s", digest, cl.digest)) + } + + if diffid != cl.diffid { + errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Gunzip(Compressed()))=%s", diffid, cl.diffid)) + } + + if diffid != cl.uncompressedDiffid { + errs = append(errs, fmt.Sprintf("mismatched diffid: DiffID()=%s, SHA256(Uncompressed())=%s", diffid, cl.uncompressedDiffid)) + } + + if size != cl.size { + errs = append(errs, fmt.Sprintf("mismatched size: Size()=%d, len(Compressed())=%d", size, cl.size)) + } + + if len(errs) != 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +type computedLayer struct { + // Calculated from Compressed stream. + digest v1.Hash + size int64 + diffid v1.Hash + + // Calculated from Uncompressed stream. + uncompressedDiffid v1.Hash + uncompressedSize int64 +} + +func computeLayer(layer v1.Layer) (*computedLayer, error) { + compressed, err := layer.Compressed() + if err != nil { + return nil, err + } + + // Keep track of compressed digest. + digester := crypto.SHA256.New() + // Everything read from compressed is written to digester to compute digest. + hashCompressed := io.TeeReader(compressed, digester) + + // Call io.Copy to write from the layer Reader through to the tarReader on + // the other side of the pipe. + pr, pw := io.Pipe() + var size int64 + go func() { + n, err := io.Copy(pw, hashCompressed) + if err != nil { + pw.CloseWithError(err) + return + } + size = n + + // Now close the compressed reader, to flush the gzip stream + // and calculate digest/diffID/size. This will cause pr to + // return EOF which will cause readers of the Compressed stream + // to finish reading. + pw.CloseWithError(compressed.Close()) + }() + + // Read the bytes through gzip.Reader to compute the DiffID. + uncompressed, err := gzip.NewReader(pr) + if err != nil { + return nil, err + } + diffider := crypto.SHA256.New() + hashUncompressed := io.TeeReader(uncompressed, diffider) + + // Ensure there aren't duplicate file paths. + tarReader := tar.NewReader(hashUncompressed) + files := make(map[string]struct{}) + for { + hdr, err := tarReader.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + if _, ok := files[hdr.Name]; ok { + return nil, fmt.Errorf("duplicate file path: %s", hdr.Name) + } + files[hdr.Name] = struct{}{} + } + + // Discard any trailing padding that the tar.Reader doesn't consume. + if _, err := io.Copy(io.Discard, hashUncompressed); err != nil { + return nil, err + } + + if err := uncompressed.Close(); err != nil { + return nil, err + } + + digest := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(digester.Sum(make([]byte, 0, digester.Size()))), + } + + diffid := v1.Hash{ + Algorithm: "sha256", + Hex: hex.EncodeToString(diffider.Sum(make([]byte, 0, diffider.Size()))), + } + + ur, err := layer.Uncompressed() + if err != nil { + return nil, err + } + defer ur.Close() + udiffid, usize, err := v1.SHA256(ur) + if err != nil { + return nil, err + } + + return &computedLayer{ + digest: digest, + diffid: diffid, + size: size, + uncompressedDiffid: udiffid, + uncompressedSize: usize, + }, nil +} diff --git a/pkg/v1/validate/options.go b/pkg/v1/validate/options.go new file mode 100644 index 0000000..a6bf2dc --- /dev/null +++ b/pkg/v1/validate/options.go @@ -0,0 +1,37 @@ +// Copyright 2021 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package validate + +// Option is a functional option for validate. +type Option func(*options) + +type options struct { + fast bool +} + +func makeOptions(opts ...Option) options { + opt := options{ + fast: false, + } + for _, o := range opts { + o(&opt) + } + return opt +} + +// Fast causes validate to skip reading and digesting layer bytes. +func Fast(o *options) { + o.fast = true +} diff --git a/pkg/v1/zz_deepcopy_generated.go b/pkg/v1/zz_deepcopy_generated.go new file mode 100644 index 0000000..a47b747 --- /dev/null +++ b/pkg/v1/zz_deepcopy_generated.go @@ -0,0 +1,339 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// Copyright 2018 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Config) DeepCopyInto(out *Config) { + *out = *in + if in.Cmd != nil { + in, out := &in.Cmd, &out.Cmd + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Healthcheck != nil { + in, out := &in.Healthcheck, &out.Healthcheck + *out = new(HealthConfig) + (*in).DeepCopyInto(*out) + } + if in.Entrypoint != nil { + in, out := &in.Entrypoint, &out.Entrypoint + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.OnBuild != nil { + in, out := &in.OnBuild, &out.OnBuild + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make(map[string]struct{}, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ExposedPorts != nil { + in, out := &in.ExposedPorts, &out.ExposedPorts + *out = make(map[string]struct{}, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Shell != nil { + in, out := &in.Shell, &out.Shell + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config. +func (in *Config) DeepCopy() *Config { + if in == nil { + return nil + } + out := new(Config) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ConfigFile) DeepCopyInto(out *ConfigFile) { + *out = *in + in.Created.DeepCopyInto(&out.Created) + if in.History != nil { + in, out := &in.History, &out.History + *out = make([]History, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.RootFS.DeepCopyInto(&out.RootFS) + in.Config.DeepCopyInto(&out.Config) + if in.OSFeatures != nil { + in, out := &in.OSFeatures, &out.OSFeatures + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigFile. +func (in *ConfigFile) DeepCopy() *ConfigFile { + if in == nil { + return nil + } + out := new(ConfigFile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Descriptor) DeepCopyInto(out *Descriptor) { + *out = *in + out.Digest = in.Digest + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.URLs != nil { + in, out := &in.URLs, &out.URLs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Platform != nil { + in, out := &in.Platform, &out.Platform + *out = new(Platform) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Descriptor. +func (in *Descriptor) DeepCopy() *Descriptor { + if in == nil { + return nil + } + out := new(Descriptor) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Hash) DeepCopyInto(out *Hash) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hash. +func (in *Hash) DeepCopy() *Hash { + if in == nil { + return nil + } + out := new(Hash) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HealthConfig) DeepCopyInto(out *HealthConfig) { + *out = *in + if in.Test != nil { + in, out := &in.Test, &out.Test + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthConfig. +func (in *HealthConfig) DeepCopy() *HealthConfig { + if in == nil { + return nil + } + out := new(HealthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *History) DeepCopyInto(out *History) { + *out = *in + in.Created.DeepCopyInto(&out.Created) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new History. +func (in *History) DeepCopy() *History { + if in == nil { + return nil + } + out := new(History) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IndexManifest) DeepCopyInto(out *IndexManifest) { + *out = *in + if in.Manifests != nil { + in, out := &in.Manifests, &out.Manifests + *out = make([]Descriptor, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Subject != nil { + in, out := &in.Subject, &out.Subject + *out = new(Descriptor) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexManifest. +func (in *IndexManifest) DeepCopy() *IndexManifest { + if in == nil { + return nil + } + out := new(IndexManifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Manifest) DeepCopyInto(out *Manifest) { + *out = *in + in.Config.DeepCopyInto(&out.Config) + if in.Layers != nil { + in, out := &in.Layers, &out.Layers + *out = make([]Descriptor, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Subject != nil { + in, out := &in.Subject, &out.Subject + *out = new(Descriptor) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Manifest. +func (in *Manifest) DeepCopy() *Manifest { + if in == nil { + return nil + } + out := new(Manifest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Platform) DeepCopyInto(out *Platform) { + *out = *in + if in.OSFeatures != nil { + in, out := &in.OSFeatures, &out.OSFeatures + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Platform. +func (in *Platform) DeepCopy() *Platform { + if in == nil { + return nil + } + out := new(Platform) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RootFS) DeepCopyInto(out *RootFS) { + *out = *in + if in.DiffIDs != nil { + in, out := &in.DiffIDs, &out.DiffIDs + *out = make([]Hash, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RootFS. +func (in *RootFS) DeepCopy() *RootFS { + if in == nil { + return nil + } + out := new(RootFS) + in.DeepCopyInto(out) + return out +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Time. +func (in *Time) DeepCopy() *Time { + if in == nil { + return nil + } + out := new(Time) + in.DeepCopyInto(out) + return out +} |