Compare commits

..
23 Commits
Author SHA1 Message Date
Paweł Gronowski dfc4efb1e2 Merge pull request #7109 from vvoland/update-docker
[29.x] Bump to 29.6.2
2026-07-16 18:09:24 +02:00
Paweł Gronowski dba867594c Bump to 29.6.2
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-16 12:51:30 +02:00
Paweł Gronowski d9aefe565a Merge tag 'v29.6.1' into 29.x
v29.6.1
2026-07-15 17:35:42 +02:00
Paweł Gronowski 9182b351c1 Merge tag 'v29.6.0' into 29.x
v29.6.0
2026-07-15 17:35:42 +02:00
Paweł Gronowski 0ae55a3e6c Merge tag 'v29.5.3' into 29.x
v29.5.3
2026-07-15 17:35:42 +02:00
Paweł Gronowski 029a4b21b3 Merge tag 'v29.5.2' into 29.x
v29.5.2
2026-07-15 17:35:41 +02:00
Paweł Gronowski e596fea454 Merge tag 'v29.5.1' into 29.x
v29.5.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski 0c72f456aa Merge tag 'v29.5.0' into 29.x
v29.5.0
2026-07-15 17:35:41 +02:00
Paweł Gronowski 26f2736a2b Merge tag 'v29.4.1' into 29.x
v29.4.1
2026-07-15 17:35:41 +02:00
Paweł Gronowski ac4c4dfabd Merge tag 'v29.4.0' into 29.x
v29.4.0
2026-07-15 17:35:40 +02:00
Sebastiaan van Stijn 1245054535 Merge pull request #7099 from vvoland/7098-29.x
[29.x backport] scripts/sync-branch: Fix non-interactive merge
2026-07-10 08:34:26 +02:00
Paweł Gronowski f72fb0caf0 scripts/sync-branch: Fix non-interactive merge
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit f57e528457)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 16:52:02 +02:00
Sebastiaan van Stijn e4af2bf0d3 Merge pull request #7097 from vvoland/7091-29.x
[29.x backport] gha: Add release branch sync workflow
2026-07-09 16:35:45 +02:00
Paweł Gronowski 49a1af6aaa gha: Add release branch sync workflow
Add a manually dispatched workflow for maintainers to sync a Docker
release branch to a selected release tag.

The sync-release-branch job checks out the release branch, computes
the list of unmerged tags up to the requested tag via
scripts/unmerged-tags, merges them in order via scripts/sync-branch
using git merge --no-ff (resolving conflicts by taking the tag's
content), then pushes the result to a temporary branch.

The push-release-branch job runs after manual approval via the
docker-releases environment. It verifies that neither the release
branch nor the temporary branch moved since the sync job ran before
force-advancing the release branch and deleting the temporary branch.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 182f56fe8c)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2026-07-09 14:45:32 +02:00
Paweł Gronowski c2be9ccfc3 Merge pull request #6887 from thaJeztah/29.x_backport_pin_actions
[29.x backport] ci: pin actions to digests
2026-03-25 15:22:32 +01:00
Sebastiaan van Stijn 0da6a5121f ci: pin actions to digests
As a follow-up, we should use the full version (major.minor.patch).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 97b9e04a94)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 15:06:23 +01:00
Sebastiaan van Stijn 6b3ca8fc04 Merge pull request #6884 from thaJeztah/29.x_backport_missing_buildtags
[29.x backport] cli/command: add missing "go:build" comments
2026-03-25 15:04:52 +01:00
Paweł Gronowski f47603c290 Merge pull request #6885 from thaJeztah/29.x_backport_update_actions
[29.x backport] update actions
2026-03-25 14:23:37 +01:00
dependabot[bot] 9709c8fe08 build(deps): bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 39d676c72d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:16:00 +01:00
dependabot[bot] af45a23fa9 build(deps): bump docker/bake-action from 6 to 7
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 6 to 7.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 6453c4c3a7)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:15:48 +01:00
Paweł Gronowski bc97f5ac19 Merge pull request #6883 from thaJeztah/29.x_backport_update_go1.25.8
[29.x backport] update to go1.25.8
2026-03-25 14:14:46 +01:00
Sebastiaan van Stijn 6d71967120 cli/command: add missing "go:build" comments
- commit e8dc2fce32 modernized loops to
  range over int, which requires go1.22 or later.
- commit 85ebca52fd modernized code to
  use stdlib min/max, which requires go1.21 or later.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9a471180cb)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:11:15 +01:00
Sebastiaan van Stijn 9b51892b80 update to go1.25.8
go1.25.8 (released 2026-03-05) includes security fixes to the html/template,
net/url, and os packages, as well as bug fixes to the go command, the compiler,
and the os package. See the Go 1.25.8 milestone on our issue tracker for details.

- 1.25.8 https://github.com/golang/go/issues?q=milestone%3AGo1.25.8+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.25.7...go1.25.8
- 1.26.1 https://github.com/golang/go/issues?q=milestone%3AGo1.26.1+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.26.0...go1.26.1

---

We have just released Go versions 1.26.1 and 1.25.8, minor point releases.

These releases include 5 security fixes following the security policy:

crypto/x509: incorrect enforcement of email constraints

- When verifying a certificate chain which contains a certificate containing
  multiple email address constraints (composed of the full email address) which
  share common local portions (the portion of the address before the '@'
  character) but different domain portions (the portion of the address after the
  '@' character), these constraints will not be properly applied, and only the
  last constraint will be considered.

  This can allow certificates in the chain containing email addresses which are
  either not permitted or excluded by the relevant constraints to be returned by
  calls to Certificate.Verify. Since the name constraint checks happen after chain
  building is complete, this only applies to certificate chains which chain to
  trusted roots (root certificates either in VerifyOptions.Roots or in the system
  root certificate pool), requiring a trusted CA to issue certificates containing
  either not permitted or excluded email addresses.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27137 and Go issue https://go.dev/issue/77952.

- crypto/x509: panic in name constraint checking for malformed certificates

  Certificate verification can panic when a certificate in the chain has an empty
  DNS name and another certificate in the chain has excluded name constraints.
  This can crash programs that are either directly verifying X.509 certificate
  chains, or those that use TLS.

  Since the name constraint checks happen after chain building is complete, this
  only applies to certificate chains which chain to trusted roots (root
  certificates either in VerifyOptions.Roots or in the system root certificate
  pool), requiring a trusted CA to issue certificates containing malformed DNS
  names.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27138 and Go issue https://go.dev/issue/77953.

- html/template: URLs in meta content attribute actions are not escaped

  Actions which insert URLs into the content attribute of HTML meta tags are not
  escaped. This can allow XSS if the meta tag also has an http-equiv attribute
  with the value "refresh".

  A new GODEBUG setting has been added, htmlmetacontenturlescape, which can be
  used to disable escaping URLs in actions in the meta content attribute which
  follow "url=" by setting htmlmetacontenturlescape=0.

  This is CVE-2026-27142 and Go issue https://go.dev/issue/77954.

- net/url: reject IPv6 literal not at start of host

  The Go standard library function net/url.Parse insufficiently
  validated the host/authority component and accepted some invalid URLs
  by effectively treating garbage before an IP-literal as ignorable.
  The function should have rejected this as invalid.

  To prevent this behavior, net/url.Parse now rejects IPv6 literals
  that do not appear at the start of the host subcomponent of a URL.

  Thanks to Masaki Hara (https://github.com/qnighy) of Wantedly.

  This is CVE-2026-25679 and Go issue https://go.dev/issue/77578.

- os: FileInfo can escape from a Root

  On Unix platforms, when listing the contents of a directory using
  File.ReadDir or File.Readdir the returned FileInfo could reference
  a file outside of the Root in which the File was opened.

  The contents of the FileInfo were populated using the lstat system
  call, which takes the path to the file as a parameter. If a component
  of the full path of the file described by the FileInfo is replaced with
  a symbolic link, the target of the lstat can be directed to another
  location on the filesystem.

  The impact of this escape is limited to reading metadata provided by
  lstat from arbitrary locations on the filesystem. This could be used
  to probe for the presence or absence of files as well as gleaning
  metadata like file sizes, but does not permit reading or writing files
  outside the root.

  The FileInfo is now populated using fstatat.

  Thank you to Miloslav Trmač of Red Hat for reporting this issue.

  This is CVE-2026-27139 and Go issue https://go.dev/issue/77827.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f7d83cbae8)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:08:46 +01:00
637 changed files with 14046 additions and 50949 deletions
-7
View File
@@ -9,10 +9,3 @@ updates:
- "status/2-code-review"
cooldown:
default-days: 7
groups:
codeql-actions:
patterns:
- "github/codeql-action/*"
docker-actions:
patterns:
- "docker/*"
+45 -31
View File
@@ -35,7 +35,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
-
@@ -65,10 +65,10 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Build
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0
with:
targets: ${{ matrix.target }}
set: |
@@ -98,32 +98,46 @@ jobs:
if-no-files-found: error
bin-image:
runs-on: ubuntu-24.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0
permissions:
contents: read # same as global permission
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
with:
setup-qemu: true
target: bin-image-cross
cache: true
cache-scope: bin-image
output: image
push: true
vars: |
VERSION=${{ github.ref }}
meta-images: |
dockereng/cli-bin
meta-tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
registry-identities: |
- type: dockerhub
username: dockereng
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
steps:
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Docker meta
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
with:
images: dockereng/cli-bin
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
-
name: Build and push image
uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0
with:
files: |
./docker-bake.hcl
cwd://${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins:
runs-on: ubuntu-24.04
@@ -132,7 +146,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
-
@@ -156,10 +170,10 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Build
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0
with:
targets: plugins-cross
set: |
+6 -6
View File
@@ -46,7 +46,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 2
persist-credentials: false
@@ -62,20 +62,20 @@ jobs:
ln -s vendor.sum go.sum
-
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "1.26.7"
go-version: "1.26.4"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:go"
+3 -3
View File
@@ -44,7 +44,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
-
@@ -65,7 +65,7 @@ jobs:
docker info
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Run ${{ matrix.target }}
run: |
@@ -76,7 +76,7 @@ jobs:
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
-9
View File
@@ -7,17 +7,8 @@ on:
permissions: {}
# Deduplicate simultaneous pull_request events for the same fork PR.
# When reviewers are requested at the same time, GitHub fires multiple
# review_requested events. Without this group each event triggers a
# separate review via workflow_run, producing duplicate reviews.
concurrency:
group: pr-review-trigger-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
save-context:
if: github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest
steps:
- name: Save event context
+2 -2
View File
@@ -11,13 +11,13 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@b08367e7e9a471822464be2bb9fdaa1c0fb6061b # v2.0.4
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@e96a4bb40cac114f64358621e1d08346c8eadc8c # v2.0.1
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
issues: write # Create security incident issues if secrets detected
checks: write # (Optional) Show review progress as a check run
id-token: write # Required for OIDC authentication to AWS Secrets Manager
actions: write # Required since v2.0.3 — review-lock cache cleanup and feedback artifacts
actions: read # Download artifacts from trigger workflow
with:
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
+11 -31
View File
@@ -1,7 +1,7 @@
name: Sync Docker release branch
concurrency:
group: ${{ github.workflow }}-${{ inputs.release_branch }}
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
permissions:
@@ -10,10 +10,6 @@ permissions:
on:
workflow_dispatch:
inputs:
release_branch:
description: Release branch to sync, for example 29.x
required: true
type: string
tag:
description: Tag to sync from, for example v29.6.0
required: true
@@ -29,7 +25,6 @@ jobs:
runs-on: ubuntu-24.04
permissions:
contents: write
actions: write
outputs:
base_sha: ${{ steps.sync.outputs.base_sha }}
has_changes: ${{ steps.sync.outputs.has_changes }}
@@ -38,25 +33,16 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Validate
env:
BRANCH: ${{ github.ref_name }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
run: |
if [ "$BRANCH" != "master" ]; then
echo "::error::This workflow is expected to be run on master, not $BRANCH"
exit 1
fi
if [ "$RELEASE_BRANCH" = "master" ]; then
echo "::error::The release branch must not be master"
exit 1
fi
if ! [[ "$RELEASE_BRANCH" =~ ^[0-9]+\.[x0-9]+$ ]]; then
echo "::error::Invalid release branch name: '$RELEASE_BRANCH'. Expected format: 29.x"
if [ "$BRANCH" = "master" ]; then
echo "::error::This workflow is expected to be run on a release branch, not master"
exit 1
fi
@@ -69,7 +55,7 @@ jobs:
id: sync
env:
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
RELEASE_BRANCH: ${{ github.ref_name }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_ID: ${{ github.run_id }}
TAG: ${{ inputs.tag }}
@@ -80,14 +66,9 @@ jobs:
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT"
# Keep the master checkout unchanged so scripts run from the dispatched revision.
release_worktree="$RUNNER_TEMP/release-branch"
git worktree add --detach "$release_worktree" "$base_sha"
cd "$release_worktree"
tags_file=$(mktemp)
"$GITHUB_WORKSPACE/scripts/unmerged-tags" \
"origin/$RELEASE_BRANCH" \
scripts/unmerged-tags \
"$RELEASE_BRANCH" \
"$TAG" \
> "$tags_file"
@@ -96,7 +77,7 @@ jobs:
echo >> "$GITHUB_STEP_SUMMARY"
sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY"
xargs -r "$GITHUB_WORKSPACE/scripts/sync-branch" < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
xargs -r scripts/sync-branch < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
if [[ "$DRY_RUN" == "true" ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
@@ -120,18 +101,17 @@ jobs:
environment: docker-releases
permissions:
contents: write
actions: write
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Checkout release
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
- name: Push release branch
env:
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
RELEASE_BRANCH: ${{ github.ref_name }}
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
run: |
+7 -7
View File
@@ -30,15 +30,15 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Test
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0
with:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
@@ -60,15 +60,15 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli
persist-credentials: false
-
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "1.26.7"
go-version: "1.26.4"
cache: false
-
name: Test
@@ -82,7 +82,7 @@ jobs:
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
+6 -6
View File
@@ -38,7 +38,7 @@ jobs:
steps:
-
name: Run
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
uses: docker/bake-action@6614cfa25eff9a0b2b2697efb0b6159e7680d584 # v7.2.0
with:
targets: ${{ matrix.target }}
@@ -48,7 +48,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
-
@@ -76,7 +76,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
-
@@ -93,15 +93,15 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
path: src/github.com/docker/cli
persist-credentials: false
-
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
with:
go-version: "1.26.7"
go-version: "1.26.4"
cache: false
-
name: Run gocompat check
+1 -1
View File
@@ -21,7 +21,7 @@ on:
jobs:
run:
uses: crazy-max/.github/.github/workflows/zizmor.yml@f91220cba053bd54299ba645a599ee8ed436f222 # v1.11.0
uses: crazy-max/.github/.github/workflows/zizmor.yml@46267a6e61cd56aac2fc79943df180152f4c89d6 # v1.10.1
permissions:
contents: read
security-events: write
+1 -1
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.26.7"
go: "1.26.4"
timeout: 5m
+1 -1
View File
@@ -267,7 +267,7 @@ Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use a known identity (sorry, no anonymous contributions.)
Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`.
+1 -1
View File
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.4
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
+1
View File
@@ -3,6 +3,7 @@
[![PkgGoDev](https://pkg.go.dev/badge/github.com/docker/cli)](https://pkg.go.dev/github.com/docker/cli)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
+1 -1
View File
@@ -1 +1 @@
29.8.0
29.6.2
+15 -33
View File
@@ -208,70 +208,52 @@ func TestInitializeFromClient(t *testing.T) {
// Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) {
const (
// Sized against measured scheduler stalls:
// under CPU pressure this test sees 40-90ms stalls;
// this should give about 5x headroom.
// See https://github.com/docker/cli/issues/6003.
clientInitTimeout = 500 * time.Millisecond
// This is only a backstop against a genuine hang.
// It should never be reached on a healthy run.
// So, it should be fine to have a lenient timeout here.
waitTimeout = 10 * time.Second
)
tmpDir := t.TempDir()
socket := filepath.Join(tmpDir, "my.sock")
l, err := net.Listen("unix", socket)
assert.NilError(t, err)
// Buffered, so the handler can record
// that it was reached without a reader
// having to be ready at that instant.
receivedReqCh := make(chan struct{}, 1)
releaseHandlerCh := make(chan struct{})
receiveReqCh := make(chan bool)
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
defer cancel()
// Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case receivedReqCh <- struct{}{}:
default:
case <-timeoutCtx.Done():
case receiveReqCh <- true: // Blocks until someone receives on the channel.
}
<-releaseHandlerCh
_, _ = w.Write([]byte("OK"))
}))
ts.Listener = l
ts.Start()
t.Cleanup(func() {
close(releaseHandlerCh)
ts.Close()
})
defer ts.Close()
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
initErrCh := make(chan error, 1)
initializedCh := make(chan bool)
go func() {
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err)
cli.CurrentVersion()
initErrCh <- err
close(initializedCh)
}()
select {
case err := <-initErrCh:
assert.Check(t, err)
case <-time.After(waitTimeout):
case <-timeoutCtx.Done():
t.Fatal("timeout waiting for initialization to complete")
case <-initializedCh:
}
select {
case <-receivedReqCh:
case <-time.After(waitTimeout):
case <-timeoutCtx.Done():
t.Fatal("server never received an init request")
case <-receiveReqCh:
}
}
-22
View File
@@ -172,28 +172,6 @@ func FromList(options ...string) cobra.CompletionFunc {
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
}
// WithPrefix prefixes every element in the slice with the given prefix.
// It is a helper for building "--filter" completions, where each candidate
// value is offered as "key=value".
func WithPrefix(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
// WithSuffix appends the given suffix to every element in the slice. It is a
// helper for building "--filter" completions, where filter keys are offered
// with a trailing "=" (combined with [cobra.ShellCompDirectiveNoSpace]).
func WithSuffix(suffix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + suffix
}
return result
}
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
// which indicates to let the shell perform its default behavior after
// completions have been provided.
-12
View File
@@ -196,18 +196,6 @@ func TestCompleteFromList(t *testing.T) {
assert.Check(t, is.DeepEqual(values, expected))
}
func TestWithPrefix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
}
func TestWithSuffix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
}
func TestCompleteImageNames(t *testing.T) {
tests := []struct {
doc string
+1 -1
View File
@@ -71,7 +71,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
flags.SetInterspersed(false)
flags.StringVar(&options.name, "name", "", "Assign a name to the container")
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "`+PullImageMissing+`", "`+PullImageNever+`")`)
flags.StringVar(&options.pull, "pull", PullImageMissing, `Pull image before creating ("`+PullImageAlways+`", "|`+PullImageMissing+`", "`+PullImageNever+`")`)
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the pull output")
flags.BoolVarP(&options.useAPISocket, "use-api-socket", "", false, "Bind mount Docker API socket and required auth")
_ = flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Mark flag as experimental for now.
+1
View File
@@ -88,6 +88,7 @@ func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error {
for _, name := range opts.containers {
if err := <-errChan; err != nil {
if opts.force && errdefs.IsNotFound(err) {
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
continue
}
errs = append(errs, err)
-1
View File
@@ -52,7 +52,6 @@ func TestRemoveForce(t *testing.T) {
} else {
assert.NilError(t, err)
}
assert.Equal(t, cli.ErrBuffer().String(), "")
sort.Strings(removed)
assert.DeepEqual(t, removed, []string{"mycontainer", "nosuchcontainer"})
})
+1 -1
View File
@@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool {
}
if i.IP != j.IP {
return i.IP.Less(j.IP)
return i.IP.String() < j.IP.String()
}
if i.PublicPort != j.PublicPort {
-18
View File
@@ -946,24 +946,6 @@ func TestDisplayablePorts(t *testing.T) {
},
expected: "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", //nolint:revive // ignore line-length-limit (revive)
},
{
// host IPs are ordered numerically, not lexicographically:
// "10.0.0.2" sorts as a string before "9.0.0.1".
ports: []container.PortSummary{
{
IP: netip.MustParseAddr("10.0.0.2"),
PublicPort: 8080,
PrivatePort: 80,
Type: "tcp",
}, {
IP: netip.MustParseAddr("9.0.0.1"),
PublicPort: 8081,
PrivatePort: 80,
Type: "tcp",
},
},
expected: "9.0.0.1:8081->80/tcp, 10.0.0.2:8080->80/tcp",
},
}
for _, port := range cases {
+23 -1
View File
@@ -48,6 +48,27 @@ func newManifestStore(dockerCLI command.Cli) store.Store {
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
}
// authConfigKey is the key used to store credentials for Docker Hub. It is
// a copy of [registry.IndexServer].
//
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"
// getAuthConfigKey special-cases using the full index address of the official
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
//
// It is similar to [registry.GetAuthConfigKey], but does not require on
// [registrytypes.IndexInfo] as intermediate.
//
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
func getAuthConfigKey(domainName string) string {
if domainName == "docker.io" || domainName == "index.docker.io" {
return authConfigKey
}
return domainName
}
// newRegistryClient returns a client for communicating with a Docker distribution
// registry
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
@@ -57,7 +78,8 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
}
cfg := dockerCLI.ConfigFile()
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
a, _ := cfg.GetAuthConfig(domainName)
configKey := getAuthConfigKey(domainName)
a, _ := cfg.GetAuthConfig(configKey)
return registry.AuthConfig{
Username: a.Username,
Password: a.Password,
-34
View File
@@ -2,26 +2,12 @@ package node
import (
"os"
"strings"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
var (
// nodePsFilters are the filters that can be used with "docker node ps --filter".
nodePsFilters = []string{"desired-state", "id", "label", "name"}
// taskDesiredStates are the valid values for the "desired-state" task filter.
taskDesiredStates = []string{
string(swarm.TaskStateRunning),
string(swarm.TaskStateShutdown),
string(swarm.TaskStateAccepted),
}
)
// completeNodeNames offers completion for swarm node (host)names and optional IDs.
// By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_NODE_IDS=yes to also complete IDs.
@@ -49,23 +35,3 @@ func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionF
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// completeNodePsFilters provides completion for the filters that can be used
// with "docker node ps --filter".
func completeNodePsFilters(_ completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
key, _, ok := strings.Cut(toComplete, "=")
if !ok {
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace
}
switch key {
case "desired-state":
return completion.WithPrefix("desired-state=", taskDesiredStates), cobra.ShellCompDirectiveNoFileComp
case "id", "name", "label":
// Task IDs, names, and labels are not easily discoverable; only offer the key.
return nil, cobra.ShellCompDirectiveNoFileComp
default:
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}
}
-52
View File
@@ -1,52 +0,0 @@
package node
import (
"testing"
"github.com/docker/cli/internal/test"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestCompleteNodePsFilters(t *testing.T) {
tests := []struct {
doc string
toComplete string
expected []string
directive cobra.ShellCompDirective
}{
{
doc: "no input offers the filter keys",
toComplete: "",
expected: []string{"desired-state=", "id=", "label=", "name="},
directive: cobra.ShellCompDirectiveNoSpace,
},
{
doc: "desired-state values",
toComplete: "desired-state=",
expected: []string{"desired-state=running", "desired-state=shutdown", "desired-state=accepted"},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "label offers no values",
toComplete: "label=",
expected: nil,
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "unknown key falls back to the filter keys",
toComplete: "bogus=",
expected: []string{"desired-state=", "id=", "label=", "name="},
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
completions, directive := completeNodePsFilters(cli)(newPsCommand(cli), nil, tc.toComplete)
assert.DeepEqual(t, completions, tc.expected)
assert.Equal(t, directive, tc.directive)
})
}
}
-2
View File
@@ -48,8 +48,6 @@ func newPsCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
_ = cmd.RegisterFlagCompletionFunc("filter", completeNodePsFilters(dockerCLI))
return cmd
}
+7 -12
View File
@@ -97,22 +97,17 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
// On Windows, force the use of the regular OS stdin stream.
//
// StdStreams() may wrap stdin with windowsconsole.NewAnsiReader to
// emulate VT input on consoles that do not support it natively, but
// that wrapper has historically caused interactive prompts to hang
// or behave incorrectly.
//
// See:
// - https://github.com/moby/moby/issues/14336
// - https://github.com/moby/moby/issues/14210
// - https://github.com/moby/moby/pull/17738
stdIn := cli.In()
//
// TODO(thaJeztah): we need to confirm if this special handling is still needed, as we may not be doing this in other places.
if runtime.GOOS == "windows" {
// TODO(thaJeztah); change to io.Reader and skip wrapping once prompt.DisableInputEcho no longer requires a streams.In
stdIn = streams.NewIn(os.Stdin)
cli.SetIn(streams.NewIn(os.Stdin))
}
argUser = strings.TrimSpace(argUser)
@@ -137,7 +132,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
}
var err error
argUser, err = prompt.ReadInput(ctx, stdIn, cli.Out(), msg)
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
if err != nil {
return registrytypes.AuthConfig{}, err
}
@@ -151,7 +146,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
isEmpty := strings.TrimSpace(argPassword) == ""
if isEmpty {
restoreInput, err := prompt.DisableInputEcho(stdIn)
restoreInput, err := prompt.DisableInputEcho(cli.In())
if err != nil {
return registrytypes.AuthConfig{}, err
}
@@ -171,7 +166,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
}
argPassword, err = prompt.ReadInput(ctx, stdIn, cli.Out(), "Password: ")
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return registrytypes.AuthConfig{}, err
}
-91
View File
@@ -2,32 +2,12 @@ package service
import (
"os"
"strings"
"github.com/docker/cli/cli/command/completion"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
)
var (
// serviceListFilters are the filters that can be used with "docker service ls --filter".
serviceListFilters = []string{"id", "label", "mode", "name"}
// serviceModes are the valid values for the "mode" filter of "docker service ls".
serviceModes = []string{"replicated", "global", "replicated-job", "global-job"}
// servicePsFilters are the filters that can be used with "docker service ps --filter".
servicePsFilters = []string{"desired-state", "id", "name", "node"}
// taskDesiredStates are the valid values for the "desired-state" task filter.
taskDesiredStates = []string{
string(swarm.TaskStateRunning),
string(swarm.TaskStateShutdown),
string(swarm.TaskStateAccepted),
}
)
// completeServiceNames offers completion for swarm service names and optional IDs.
// By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_SERVICE_IDS=yes to also complete IDs.
@@ -51,74 +31,3 @@ func completeServiceNames(dockerCLI completion.APIClientProvider) cobra.Completi
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// completeServiceListFilters provides completion for the filters that can be
// used with "docker service ls --filter".
func completeServiceListFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
key, _, ok := strings.Cut(toComplete, "=")
if !ok {
return completion.WithSuffix("=", serviceListFilters), cobra.ShellCompDirectiveNoSpace
}
switch key {
case "id", "name":
return completion.WithPrefix(key+"=", serviceNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "mode":
return completion.WithPrefix("mode=", serviceModes), cobra.ShellCompDirectiveNoFileComp
case "label":
return nil, cobra.ShellCompDirectiveNoFileComp
default:
return completion.WithSuffix("=", serviceListFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}
}
// completeServicePsFilters provides completion for the filters that can be
// used with "docker service ps --filter".
func completeServicePsFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
key, _, ok := strings.Cut(toComplete, "=")
if !ok {
return completion.WithSuffix("=", servicePsFilters), cobra.ShellCompDirectiveNoSpace
}
switch key {
case "desired-state":
return completion.WithPrefix("desired-state=", taskDesiredStates), cobra.ShellCompDirectiveNoFileComp
case "node":
return completion.WithPrefix("node=", nodeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
case "id", "name":
// Task IDs and names are not easily discoverable; only offer the key.
return nil, cobra.ShellCompDirectiveNoFileComp
default:
return completion.WithSuffix("=", servicePsFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
}
}
// serviceNames contacts the API to get a list of service names.
// In case of an error, an empty list is returned.
func serviceNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
res, err := dockerCLI.Client().ServiceList(cmd.Context(), client.ServiceListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(res.Items))
for _, service := range res.Items {
names = append(names, service.Spec.Name)
}
return names
}
// nodeNames contacts the API to get a list of node (host)names.
// In case of an error, an empty list is returned.
func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
res, err := dockerCLI.Client().NodeList(cmd.Context(), client.NodeListOptions{})
if err != nil {
return []string{}
}
names := make([]string, 0, len(res.Items))
for _, node := range res.Items {
names = append(names, node.Description.Hostname)
}
return names
}
-156
View File
@@ -1,156 +0,0 @@
package service
import (
"context"
"errors"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestCompleteServicePsFilters(t *testing.T) {
tests := []struct {
doc string
client *fakeClient
toComplete string
expected []string
directive cobra.ShellCompDirective
}{
{
doc: "no input offers the filter keys",
toComplete: "",
expected: []string{"desired-state=", "id=", "name=", "node="},
directive: cobra.ShellCompDirectiveNoSpace,
},
{
doc: "desired-state values",
toComplete: "desired-state=",
expected: []string{"desired-state=running", "desired-state=shutdown", "desired-state=accepted"},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "node values",
client: &fakeClient{
nodeListFunc: func(_ context.Context, _ client.NodeListOptions) (client.NodeListResult, error) {
return client.NodeListResult{
Items: []swarm.Node{
*builders.Node(builders.Hostname("n1")),
*builders.Node(builders.Hostname("n2")),
},
}, nil
},
},
toComplete: "node=",
expected: []string{"node=n1", "node=n2"},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "node values on API error",
client: &fakeClient{
nodeListFunc: func(_ context.Context, _ client.NodeListOptions) (client.NodeListResult, error) {
return client.NodeListResult{}, errors.New("API error")
},
},
toComplete: "node=",
expected: []string{},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "id offers no values",
toComplete: "id=",
expected: nil,
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "unknown key falls back to the filter keys",
toComplete: "bogus=",
expected: []string{"desired-state=", "id=", "name=", "node="},
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(tc.client)
completions, directive := completeServicePsFilters(cli)(newPsCommand(cli), nil, tc.toComplete)
assert.DeepEqual(t, completions, tc.expected)
assert.Equal(t, directive, tc.directive)
})
}
}
func TestCompleteServiceListFilters(t *testing.T) {
tests := []struct {
doc string
client *fakeClient
toComplete string
expected []string
directive cobra.ShellCompDirective
}{
{
doc: "no input offers the filter keys",
toComplete: "",
expected: []string{"id=", "label=", "mode=", "name="},
directive: cobra.ShellCompDirectiveNoSpace,
},
{
doc: "mode values",
toComplete: "mode=",
expected: []string{"mode=replicated", "mode=global", "mode=replicated-job", "mode=global-job"},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "name values",
client: &fakeClient{
serviceListFunc: func(_ context.Context, _ client.ServiceListOptions) (client.ServiceListResult, error) {
return client.ServiceListResult{
Items: []swarm.Service{
*builders.Service(builders.ServiceName("s1")),
*builders.Service(builders.ServiceName("s2")),
},
}, nil
},
},
toComplete: "name=",
expected: []string{"name=s1", "name=s2"},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "name values on API error",
client: &fakeClient{
serviceListFunc: func(_ context.Context, _ client.ServiceListOptions) (client.ServiceListResult, error) {
return client.ServiceListResult{}, errors.New("API error")
},
},
toComplete: "name=",
expected: []string{},
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "label offers no values",
toComplete: "label=",
expected: nil,
directive: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "unknown key falls back to the filter keys",
toComplete: "bogus=",
expected: []string{"id=", "label=", "mode=", "name="},
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
cli := test.NewFakeCli(tc.client)
completions, directive := completeServiceListFilters(cli)(newListCommand(cli), nil, tc.toComplete)
assert.DeepEqual(t, completions, tc.expected)
assert.Equal(t, directive, tc.directive)
})
}
}
-2
View File
@@ -38,8 +38,6 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp)
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
_ = cmd.RegisterFlagCompletionFunc("filter", completeServiceListFilters(dockerCLI))
return cmd
}
+8 -7
View File
@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/netip"
"slices"
"sort"
"strconv"
"strings"
@@ -688,13 +687,15 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
}
currentEnv := make([]string, 0, len(envVariables))
for _, env := range envVariables { // need to process each var, in order
if slices.Contains(currentEnv, env) {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
k, _, _ := strings.Cut(env, "=")
currentEnv = slices.DeleteFunc(currentEnv, func(current string) bool { // remove duplicates
return strings.HasPrefix(current, k+"=")
})
for i, current := range currentEnv { // remove duplicates
if current == env {
continue // no update required, may hide this behind flag to preserve order of envVariables
}
if strings.HasPrefix(current, k+"=") {
currentEnv = append(currentEnv[:i], currentEnv[i+1:]...)
}
}
currentEnv = append(currentEnv, env)
}
-46
View File
@@ -373,49 +373,3 @@ func TestToServiceSysCtls(t *testing.T) {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(service.TaskTemplate.ContainerSpec.Sysctls, expected))
}
func TestMakeEnv(t *testing.T) {
tests := []struct {
doc string
env []string
expected []string
}{
{
doc: "no duplicates",
env: []string{"one=1", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "same variable repeated",
env: []string{"one=1", "one=1"},
expected: []string{"one=1"},
},
{
doc: "same variable repeated, then overridden",
env: []string{"one=1", "one=1", "one=2"},
expected: []string{"one=2"},
},
{
doc: "repeated variable last",
env: []string{"one=1", "two=2", "two=2"},
expected: []string{"one=1", "two=2"},
},
{
doc: "last value wins",
env: []string{"one=1", "two=2", "one=3"},
expected: []string{"two=2", "one=3"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
o := newServiceOptions()
for _, env := range tc.env {
assert.NilError(t, o.env.Set(env))
}
actual, err := o.makeEnv()
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(tc.expected, actual))
})
}
}
-2
View File
@@ -45,8 +45,6 @@ func newPsCommand(dockerCLI command.Cli) *cobra.Command {
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
_ = cmd.RegisterFlagCompletionFunc("filter", completeServicePsFilters(dockerCLI))
return cmd
}
+5 -3
View File
@@ -1213,9 +1213,11 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
if rm.IPAddr != "" && rm.IPAddr != ip {
continue
}
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
return h == rm.Host
})
for i, h := range hostNames {
if h == rm.Host {
hostNames = append(hostNames[:i], hostNames[i+1:]...)
}
}
}
if len(hostNames) > 0 {
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
-15
View File
@@ -1727,18 +1727,3 @@ func TestUpdateUlimits(t *testing.T) {
})
}
}
func TestUpdateHostsRemoveRepeatedHost(t *testing.T) {
flags := newUpdateCommand(nil).Flags()
flags.Set("host-rm", "host1")
//nolint:dupword // ignore "Duplicate words (host1) found"
hosts := []string{"127.0.0.1 host1 host1 host2", "127.0.0.2 host2 host1 host1"}
err := updateHosts(flags, &hosts)
assert.NilError(t, err)
// All occurrences of `host1` should be removed, also if the same host
// is listed multiple times in the same entry.
assert.Check(t, is.DeepEqual([]string{"127.0.0.1 host2", "127.0.0.2 host2"}, hosts))
}
+15 -15
View File
@@ -419,7 +419,7 @@ services:
foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "version: must be a string"))
assert.Check(t, is.ErrorContains(err, "version must be a string"))
}
func TestV1Unsupported(t *testing.T) {
@@ -427,7 +427,7 @@ func TestV1Unsupported(t *testing.T) {
foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "(root): additional property 'foo' is not allowed"))
assert.Check(t, is.ErrorContains(err, "(root) Additional property foo is not allowed"))
_, err = loadYAML(`
version: "1.0"
@@ -445,14 +445,14 @@ services:
- foo:
image: busybox
`)
assert.Check(t, is.ErrorContains(err, "services: must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services must be a mapping"))
_, err = loadYAML(`
version: "3"
services:
foo: busybox
`)
assert.Check(t, is.ErrorContains(err, "services.foo: must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services.foo must be a mapping"))
_, err = loadYAML(`
version: "3"
@@ -460,14 +460,14 @@ networks:
- default:
driver: bridge
`)
assert.Check(t, is.ErrorContains(err, "networks: must be a mapping"))
assert.Check(t, is.ErrorContains(err, "networks must be a mapping"))
_, err = loadYAML(`
version: "3"
networks:
default: bridge
`)
assert.Check(t, is.ErrorContains(err, "networks.default: must be null or a mapping"))
assert.Check(t, is.ErrorContains(err, "networks.default must be a mapping"))
_, err = loadYAML(`
version: "3"
@@ -475,14 +475,14 @@ volumes:
- data:
driver: local
`)
assert.Check(t, is.ErrorContains(err, "volumes: must be a mapping"))
assert.Check(t, is.ErrorContains(err, "volumes must be a mapping"))
_, err = loadYAML(`
version: "3"
volumes:
data: local
`)
assert.Check(t, is.ErrorContains(err, "volumes.data: must be null or a mapping"))
assert.Check(t, is.ErrorContains(err, "volumes.data must be a mapping"))
}
func TestNonStringImage(t *testing.T) {
@@ -492,7 +492,7 @@ services:
foo:
image: ["busybox", "latest"]
`)
assert.Check(t, is.ErrorContains(err, "services.foo.image: must be a string"))
assert.Check(t, is.ErrorContains(err, "services.foo.image must be a string"))
}
func TestIgnoreBuildProperties(t *testing.T) {
@@ -553,7 +553,7 @@ services:
environment:
FOO: ["1"]
`)
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment.FOO: must be null or a number or a string"))
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment.FOO must be a string, number or null"))
}
func TestInvalidEnvironmentObject(t *testing.T) {
@@ -564,7 +564,7 @@ services:
image: busybox
environment: "FOO=1"
`)
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment: must be a mapping"))
assert.Check(t, is.ErrorContains(err, "services.dict-env.environment must be a mapping"))
}
func TestLoadWithEnvironmentInterpolation(t *testing.T) {
@@ -899,7 +899,7 @@ func TestInvalidResource(t *testing.T) {
impossible:
x: 1
`)
assert.Check(t, is.ErrorContains(err, "additional property 'impossible' is not allowed"))
assert.Check(t, is.ErrorContains(err, "Additional property impossible is not allowed"))
}
func TestInvalidExternalAndDriverCombination(t *testing.T) {
@@ -1033,7 +1033,7 @@ services:
tmpfs:
size: 10000
`)
assert.Check(t, is.ErrorContains(err, "services.tmpfs.volumes.0: additional property 'tmpfs' is not allowed"))
assert.Check(t, is.ErrorContains(err, "services.tmpfs.volumes.0 Additional property tmpfs is not allowed"))
}
func TestLoadBindMountSourceMustNotBeEmpty(t *testing.T) {
@@ -1191,7 +1191,7 @@ services:
tmpfs:
size: -1
`)
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size: must be greater than or equal to 0")
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size Must be greater than or equal to 0")
}
func TestLoadTmpfsVolumeSizeMustBeInteger(t *testing.T) {
@@ -1206,7 +1206,7 @@ services:
tmpfs:
size: 0.0001
`)
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size: must be an integer")
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size must be a integer")
}
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
+67 -167
View File
@@ -4,20 +4,14 @@
package schema
import (
"bytes"
"embed"
"encoding/json"
"errors"
"fmt"
"reflect"
"math/big"
"strings"
"time"
"github.com/docker/go-connections/nat"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/santhosh-tekuri/jsonschema/v6/kind"
"golang.org/x/text/language"
"golang.org/x/text/message"
"github.com/xeipuuv/gojsonschema"
)
const (
@@ -27,29 +21,34 @@ const (
type portsFormatChecker struct{}
func (portsFormatChecker) Validate(input any) error {
switch input.(type) {
case string, json.Number,
int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64,
float32, float64:
_, err := nat.ParsePortSpec(fmt.Sprint(input))
return err
default:
// Formats only apply to supported string/number values.
return nil
func (portsFormatChecker) IsFormat(input any) bool {
var portSpec string
switch p := input.(type) {
case string:
portSpec = p
case *big.Rat:
portSpec = strings.Split(p.String(), "/")[0]
}
_, err := nat.ParsePortSpec(portSpec)
return err == nil
}
type durationFormatChecker struct{}
func (durationFormatChecker) Validate(input any) error {
func (durationFormatChecker) IsFormat(input any) bool {
value, ok := input.(string)
if !ok {
return nil
return false
}
_, err := time.ParseDuration(value)
return err
return err == nil
}
func init() {
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{})
}
// Version returns the version of the config, defaulting to the latest "3.x"
@@ -83,84 +82,44 @@ func Validate(config map[string]any, version string) error {
return fmt.Errorf("unsupported Compose file version: %s", version)
}
schemaDoc, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaData))
schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
dataLoader := gojsonschema.NewGoLoader(config)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
if err != nil {
return err
}
compiler := jsonschema.NewCompiler()
compiler.RegisterFormat(&jsonschema.Format{Name: "ports", Validate: portsFormatChecker{}.Validate})
compiler.RegisterFormat(&jsonschema.Format{Name: "duration", Validate: durationFormatChecker{}.Validate})
if err := compiler.AddResource("schema.json", schemaDoc); err != nil {
return err
}
schema, err := compiler.Compile("schema.json")
if err != nil {
return err
}
if err := schema.Validate(toJSONValue(config)); err != nil {
var validationErr *jsonschema.ValidationError
if errors.As(err, &validationErr) {
return getMostSpecificError(validationErr)
}
return err
if !result.Valid() {
return toError(result)
}
return nil
}
func toJSONValue(v any) any {
if v == nil {
return nil
}
switch value := reflect.ValueOf(v); value.Kind() { //nolint: exhaustive // only need to handle maps and slices.
case reflect.Map:
result := make(map[string]any, value.Len())
iter := value.MapRange()
for iter.Next() {
result[iter.Key().String()] = toJSONValue(iter.Value().Interface())
}
return result
case reflect.Slice:
result := make([]any, value.Len())
for i := range value.Len() {
result[i] = toJSONValue(value.Index(i).Interface())
}
return result
default:
return v
}
func toError(result *gojsonschema.Result) error {
err := getMostSpecificError(result.Errors())
return err
}
var printer = message.NewPrinter(language.English)
const (
jsonschemaOneOf = "number_one_of"
jsonschemaAnyOf = "number_any_of"
)
func getDescription(err validationError) string {
switch parent := err.parent.ErrorKind.(type) {
case *kind.Type:
types := make([]string, len(parent.Want))
for i, typ := range parent.Want {
types[i] = humanReadableType(typ)
switch err.parent.Type() {
case "invalid_type":
if expectedType, ok := err.parent.Details()["expected"].(string); ok {
return "must be a " + humanReadableType(expectedType)
}
return "must be " + strings.Join(types, " or ")
case *kind.AnyOf, *kind.OneOf:
if err.child != nil {
return getDescription(validationError{parent: err.child})
case jsonschemaOneOf, jsonschemaAnyOf:
if err.child == nil {
return err.parent.Description()
}
return err.parent.ErrorKind.LocalizedString(printer)
case *kind.AdditionalProperties:
if len(parent.Properties) == 1 {
return fmt.Sprintf("additional property '%s' is not allowed", parent.Properties[0])
}
return err.parent.ErrorKind.LocalizedString(printer)
case *kind.Minimum:
want, _ := parent.Want.Float64()
return fmt.Sprintf("must be greater than or equal to %v", want)
default:
return err.parent.ErrorKind.LocalizedString(printer)
return err.child.Description()
}
return err.parent.Description()
}
func humanReadableType(definition string) string {
@@ -175,39 +134,26 @@ func humanReadableType(definition string) string {
allTypes[len(allTypes)-1],
)
}
switch definition {
case "object":
return "a mapping"
case "array":
return "a list"
case "integer":
return "an integer"
case "null":
return "null"
default:
return "a " + definition
if definition == "object" {
return "mapping"
}
if definition == "array" {
return "list"
}
return definition
}
type validationError struct {
parent *jsonschema.ValidationError
child *jsonschema.ValidationError
parent gojsonschema.ResultError
child gojsonschema.ResultError
}
func (err validationError) Error() string {
validationErr := err.parent
if err.child != nil {
validationErr = err.child
}
field := strings.Join(validationErr.InstanceLocation, ".")
if field == "" {
field = "(root)"
}
return field + ": " + getDescription(err)
description := getDescription(err)
return fmt.Sprintf("%s %s", err.parent.Field(), description)
}
func getMostSpecificError(result *jsonschema.ValidationError) validationError {
errs := flattenErrors(result)
func getMostSpecificError(errs []gojsonschema.ResultError) validationError {
mostSpecificError := 0
for i, err := range errs {
if specificity(err) > specificity(errs[mostSpecificError]) {
@@ -216,74 +162,28 @@ func getMostSpecificError(result *jsonschema.ValidationError) validationError {
}
if specificity(err) == specificity(errs[mostSpecificError]) {
// Invalid type errors win in a tie-breaker for most specific field name.
if isTypeError(err) && !isTypeError(errs[mostSpecificError]) {
// Invalid type errors win in a tie-breaker for most specific field name
if err.Type() == "invalid_type" && errs[mostSpecificError].Type() != "invalid_type" {
mostSpecificError = i
}
}
}
err := validationError{parent: errs[mostSpecificError]}
switch err.parent.ErrorKind.(type) {
case *kind.OneOf, *kind.AnyOf:
err.child = mostSpecificCause(err.parent)
if mostSpecificError+1 == len(errs) {
return validationError{parent: errs[mostSpecificError]}
}
return err
}
func mostSpecificCause(err *jsonschema.ValidationError) *jsonschema.ValidationError {
var best *jsonschema.ValidationError
for _, cause := range err.Causes {
for _, candidate := range flattenErrors(cause) {
switch candidate.ErrorKind.(type) {
case *kind.OneOf, *kind.AnyOf:
if nested := mostSpecificCause(candidate); nested != nil {
candidate = nested
}
}
if best == nil || specificity(candidate) > specificity(best) {
best = candidate
continue
}
if specificity(candidate) == specificity(best) {
// Within oneOf/anyOf, an error from an alternative whose
// type matched is more useful than a type mismatch from
// another alternative.
if !isTypeError(candidate) && isTypeError(best) {
best = candidate
}
}
switch errs[mostSpecificError].Type() {
case "number_one_of", "number_any_of":
return validationError{
parent: errs[mostSpecificError],
child: errs[mostSpecificError+1],
}
default:
return validationError{parent: errs[mostSpecificError]}
}
return best
}
func flattenErrors(err *jsonschema.ValidationError) []*jsonschema.ValidationError {
var errs []*jsonschema.ValidationError
var walk func(*jsonschema.ValidationError)
walk = func(err *jsonschema.ValidationError) {
switch err.ErrorKind.(type) {
case *kind.Schema, *kind.Group, *kind.Reference:
for _, cause := range err.Causes {
walk(cause)
}
default:
errs = append(errs, err)
}
}
walk(err)
return errs
}
func isTypeError(err *jsonschema.ValidationError) bool {
_, ok := err.ErrorKind.(*kind.Type)
return ok
}
func specificity(err *jsonschema.ValidationError) int {
return len(err.InstanceLocation)
func specificity(err gojsonschema.ResultError) int {
return len(strings.Split(err.Field(), "."))
}
+3 -16
View File
@@ -4,7 +4,6 @@
package schema
import (
"encoding/json"
"fmt"
"testing"
@@ -115,18 +114,6 @@ func TestValidatePorts(t *testing.T) {
ports: []string{"8006-8000:8005-8000"},
hasError: true,
},
{
ports: []any{json.Number("8080")},
hasError: false,
},
{
ports: []any{json.Number("65536")},
hasError: true,
},
{
ports: []any{json.Number("1.5")},
hasError: true,
},
}
for _, tc := range testcases {
@@ -140,7 +127,7 @@ func TestValidatePorts(t *testing.T) {
},
}
if tc.hasError {
assert.ErrorContains(t, Validate(config, "3"), "is not valid ports")
assert.ErrorContains(t, Validate(config, "3"), "services.foo.ports.0 Does not match format 'ports'")
} else {
assert.NilError(t, Validate(config, "3"))
}
@@ -159,7 +146,7 @@ func TestValidateUndefinedTopLevelOption(t *testing.T) {
}
err := Validate(config, "3.0")
assert.ErrorContains(t, err, "additional property 'helicopters' is not allowed")
assert.ErrorContains(t, err, "Additional property helicopters is not allowed")
}
func TestValidateAllowsXTopLevelFields(t *testing.T) {
@@ -243,7 +230,7 @@ func TestValidateCredentialSpecs(t *testing.T) {
}
err := Validate(config, tc.version)
if tc.expectedErr != "" {
assert.ErrorContains(t, err, fmt.Sprintf("additional property '%s' is not allowed", tc.expectedErr))
assert.ErrorContains(t, err, fmt.Sprintf("Additional property %s is not allowed", tc.expectedErr))
} else {
assert.NilError(t, err)
}
+1 -42
View File
@@ -104,7 +104,7 @@ func (c *fileStore) Store(authConfig types.AuthConfig) error {
// stored as hostname or as hostname including scheme (in legacy configuration
// files).
//
// It's based on [registry.ConvertToHostname] from Moby daemon.
// It's the equivalent to [registry.ConvertToHostname] in the daemon.
//
// [registry.ConvertToHostname]: https://pkg.go.dev/github.com/moby/moby/v2@v2.0.0-beta.7/daemon/pkg/registry#ConvertToHostname
func ConvertToHostname(maybeURL string) string {
@@ -117,48 +117,7 @@ func ConvertToHostname(maybeURL string) string {
}
return net.JoinHostPort(u.Hostname(), u.Port())
}
if hostName := hostFromURLFallback(stripped); hostName != "" {
return hostName
}
}
hostName, _, _ := strings.Cut(stripped, "/")
return hostName
}
// hostFromURLFallback extracts a host from scheme URLs that net/url rejects.
// Go rejects unbracketed IPv6 literals in URL hosts since
// https://github.com/golang/go/commit/0c28789bd7dfc55099cac86a3212dda0d6c091f6
func hostFromURLFallback(maybeURL string) string {
_, rest, ok := strings.Cut(maybeURL, "://")
if !ok {
return ""
}
hostName, _, _ := strings.Cut(rest, "/")
if hostName == "" {
return ""
}
if strings.Count(hostName, ":") > 1 && !strings.HasPrefix(hostName, "[") {
portStart := strings.LastIndex(hostName, ":")
addr, port := hostName[:portStart], hostName[portStart+1:]
if addr != "" && isPort(port) {
return net.JoinHostPort(addr, port)
}
}
return hostName
}
func isPort(port string) bool {
if port == "" {
return false
}
for _, r := range port {
if r < '0' || r > '9' {
return false
}
}
return true
}
+5 -5
View File
@@ -65,11 +65,11 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.54.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
+10 -10
View File
@@ -265,14 +265,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
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=
@@ -285,14 +285,14 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+3 -4
View File
@@ -8,7 +8,6 @@
DOCKER_CLI_MOUNTS ?= -v "$(CURDIR)":/go/src/github.com/docker/cli
DOCKER_CLI_CONTAINER_NAME ?=
DOCKER_CLI_GO_BUILD_CACHE ?= y
DOCKER_SOCK ?= $(or $(patsubst unix://%,%,$(filter unix://%,$(shell docker context inspect --format '{{.Endpoints.docker.Host}}'))),/var/run/docker.sock)
# Sets the name of the company that produced the windows binary.
PACKAGER_NAME ?=
@@ -63,7 +62,7 @@ dynbinary: ## build dynamically linked binary
.PHONY: dev
dev: build_docker_image ## start a build container in interactive mode for in-container development
$(DOCKER_RUN) -it \
--mount 'type=bind,src=$(DOCKER_SOCK),dst=/var/run/docker.sock' \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(DEV_DOCKER_IMAGE_NAME)
shell: dev ## alias for dev
@@ -135,14 +134,14 @@ test-e2e: test-e2e-local test-e2e-connhelper-ssh ## run all e2e tests
test-e2e-local: build-e2e-image # run experimental e2e tests
docker run --rm $(ENVVARS) \
--mount type=bind,src=$(CURDIR)/build/coverage,dst=/tmp/coverage \
--mount 'type=bind,src=$(DOCKER_SOCK),dst=/var/run/docker.sock' \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(E2E_IMAGE_NAME)
.PHONY: test-e2e-connhelper-ssh
test-e2e-connhelper-ssh: build-e2e-image # run experimental SSH-connection helper e2e tests
docker run --rm $(ENVVARS) -e TEST_CONNHELPER=ssh \
--mount type=bind,src=$(CURDIR)/build/coverage,dst=/tmp/coverage \
--mount 'type=bind,src=$(DOCKER_SOCK),dst=/var/run/docker.sock' \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
$(E2E_IMAGE_NAME)
.PHONY: help
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.4
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.4
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+1 -1
View File
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.26.7
ARG GO_VERSION=1.26.4
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
+9 -12
View File
@@ -95,18 +95,6 @@ The Engine's authorization middleware fails closed: when a plugin returns an err
the request is denied and the error is surfaced to the client. Plugins should also fail closed: if the plugin
cannot confidently evaluate a request, it should return an error or `Allow: false`.
> [!WARNING]
> Because the plugin receives the [**raw** request body](#authzpluginauthzreq) from the daemon, it must
> apply the same decoding semantics as the daemon to be sure it evaluates the request the daemon will
> act on. The daemon decodes JSON with Go's [`encoding/json.Unmarshal`](https://pkg.go.dev/encoding/json#Unmarshal).
>
> The same requirement applies to the response body. Plugins that depend on `ResponseBody`
> inspection for redaction or content-filtering should restrict their policies to endpoints
> whose response is produced as a single write (typical of REST-style API responses). For
> commands whose responses are streamed or are likely to exceed the [buffer](#response-body-size-and-partial-buffering) through multiple
> writes, do not rely on `ResponseBody` for security-relevant decisions; perform the filtering
> in a separate layer in front of the daemon.
### Response body size and partial buffering
The internal buffer that holds the response body between the daemon's HTTP
@@ -123,6 +111,15 @@ is the practical effect of this 64 KiB threshold combined with the
is immediately drained to the client and is therefore no longer available
for plugin inspection by the time the handler returns.
> [!NOTE]
> Plugins that depend on `ResponseBody` inspection for redaction or
> content-filtering should restrict their policies to endpoints whose
> response is produced as a single write (typical of REST-style API
> responses). For commands whose responses are streamed or are likely to
> exceed the buffer through multiple writes, do not rely on `ResponseBody`
> for security-relevant decisions; perform the filtering in a separate
> layer in front of the daemon.
During request/response processing, some authorization flows might
need to do additional queries to the Docker daemon. To complete such flows,
plugins can call the daemon API similar to a regular user. To enable these
@@ -87,7 +87,7 @@ Create a new container
| `--privileged` | `bool` | | Give extended privileges to this container |
| `-p`, `--publish` | `list` | | Publish a container's port(s) to the host |
| `-P`, `--publish-all` | `bool` | | Publish all exposed ports to random ports |
| `--pull` | `string` | `missing` | Pull image before creating (`always`, `missing`, `never`) |
| `--pull` | `string` | `missing` | Pull image before creating (`always`, `\|missing`, `never`) |
| `-q`, `--quiet` | `bool` | | Suppress the pull output |
| `--read-only` | `bool` | | Mount the container's root filesystem as read only |
| `--restart` | `string` | `no` | Restart policy to apply when a container exits |
+1 -1
View File
@@ -87,7 +87,7 @@ Create a new container
| `--privileged` | `bool` | | Give extended privileges to this container |
| `-p`, `--publish` | `list` | | Publish a container's port(s) to the host |
| `-P`, `--publish-all` | `bool` | | Publish all exposed ports to random ports |
| `--pull` | `string` | `missing` | Pull image before creating (`always`, `missing`, `never`) |
| `--pull` | `string` | `missing` | Pull image before creating (`always`, `\|missing`, `never`) |
| `-q`, `--quiet` | `bool` | | Suppress the pull output |
| `--read-only` | `bool` | | Mount the container's root filesystem as read only |
| `--restart` | `string` | `no` | Restart policy to apply when a container exits |
+1 -1
View File
@@ -431,7 +431,7 @@ a `docker` command. You can use the following protocols:
| Scheme | Description | Example |
|----------------------------------------|---------------------------|----------------------------------|
| `unix://[<path>]` | Unix socket | `unix:///var/run/docker.sock` |
| `unix://[<path>]` | Unix socket (Linux only) | `unix:///var/run/docker.sock` |
| `tcp://[<IP or host>[:port]]` | TCP connection | `tcp://174.17.0.1:2376` |
| `ssh://[username@]<IP or host>[:port]` | SSH connection | `ssh://user@192.168.64.5` |
| `npipe://[<name>]` | Named pipe (Windows only) | `npipe:////./pipe/docker_engine` |
-5
View File
@@ -1,5 +0,0 @@
# Generated by gen-certs.sh at setup time
testdata/registry/certs/ca.crt
testdata/registry/certs/ca.key
testdata/registry/certs/tlsregistry.crt
testdata/registry/certs/tlsregistry.key
+2 -28
View File
@@ -3,35 +3,9 @@ services:
registry:
image: 'registry:3'
privateregistry:
build:
context: ./testdata/registry
environment:
- REGISTRY_HTTP_ADDR=0.0.0.0:5001
- REGISTRY_HTTP_DEBUG_ADDR=0.0.0.0:5002
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
tlsregistry:
build:
context: ./testdata/registry
environment:
- REGISTRY_HTTP_ADDR=0.0.0.0:5003
- REGISTRY_HTTP_DEBUG_ADDR=0.0.0.0:5004
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
- REGISTRY_HTTP_TLS_CERTIFICATE=/certs/tlsregistry.crt
- REGISTRY_HTTP_TLS_KEY=/certs/tlsregistry.key
engine:
build:
context: ./testdata
dockerfile: engine/Dockerfile
args:
ENGINE_VERSION: ${ENGINE_VERSION:-29}
image: 'docker:${ENGINE_VERSION:-29}-dind'
privileged: true
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
command: ['--insecure-registry=registry:5000', '--experimental']
environment:
- DOCKER_TLS_CERTDIR=
+1 -5
View File
@@ -209,11 +209,7 @@ func TestProcessTermination(t *testing.T) {
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGTERM))
// Use a generous timeout (20s) because when run through SSH connhelper,
// the Docker engine may take longer to close the attach stream after
// the container exits. This is a known timing difference across engine
// versions (e.g. engine 25 over SSH connhelper).
icmd.WaitOnCmd(time.Second*20, result).Assert(t, icmd.Expected{
icmd.WaitOnCmd(time.Second*10, result).Assert(t, icmd.Expected{
ExitCode: 0,
})
}
-126
View File
@@ -1,126 +0,0 @@
package image
import (
"strings"
"testing"
"time"
"github.com/docker/cli/e2e/internal/fixtures"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
// Regression test for https://github.com/docker/cli/issues/5963
func TestPullPushPrivateRepository(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
registryPrefix string
tagSuffix string
}{
{name: "insecure", registryPrefix: "privateregistry:5001", tagSuffix: "private"},
{name: "tls", registryPrefix: "tlsregistry:5003", tagSuffix: "tls"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dir := fixtures.SetupConfigFile(t)
t.Cleanup(dir.Remove)
emptyConfigDir := t.TempDir()
sourceImage := fixtures.AlpineImage
privateImage := tc.registryPrefix + "/private/alpine:test-" + tc.tagSuffix + "-pull-push"
runWithPrivateRegistryRetry(t,
icmd.Command("docker", "pull", sourceImage),
).Assert(t, icmd.Success)
t.Cleanup(func() {
icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success)
})
icmd.RunCommand("docker", "tag", sourceImage, privateImage).Assert(t, icmd.Success)
pushNoAuth := runWithPrivateRegistryRetry(t,
icmd.Command("docker", "push", privateImage),
fixtures.WithConfig(emptyConfigDir),
)
pushNoAuth.Assert(t, icmd.Expected{ExitCode: 1})
assertAuthDenied(t, pushNoAuth)
pushWithAuth := runWithPrivateRegistryRetry(t,
icmd.Command("docker", "push", privateImage),
fixtures.WithConfig(dir.Path()),
)
pushWithAuth.Assert(t, icmd.Success)
// Docker omits the tag in the "push refers to repository" line; strip it before asserting.
privateRepo := privateImage[:strings.LastIndex(privateImage, ":")]
assert.Check(t, strings.Contains(pushWithAuth.Combined(), "The push refers to repository ["+privateRepo+"]"), pushWithAuth.Combined())
icmd.RunCommand("docker", "image", "rm", "-f", privateImage).Assert(t, icmd.Success)
pullNoAuth := runWithPrivateRegistryRetry(t,
icmd.Command("docker", "pull", privateImage),
fixtures.WithConfig(emptyConfigDir),
)
pullNoAuth.Assert(t, icmd.Expected{ExitCode: 1})
assertAuthDenied(t, pullNoAuth)
pullWithAuth := runWithPrivateRegistryRetry(t,
icmd.Command("docker", "pull", privateImage),
fixtures.WithConfig(dir.Path()),
)
pullWithAuth.Assert(t, icmd.Success)
assert.Check(t, strings.Contains(pullWithAuth.Combined(), privateImage), pullWithAuth.Combined())
})
}
}
func assertAuthDenied(t *testing.T, result *icmd.Result) {
t.Helper()
output := result.Combined()
if isPrivateRegistryTransient(output) {
t.Fatalf("private registry unavailable while expecting auth failure: %s", output)
}
assert.Assert(t,
strings.Contains(output, "requested access to the resource is denied") ||
strings.Contains(output, "no basic auth credentials") ||
strings.Contains(output, "unauthorized") ||
strings.Contains(output, "authentication required"),
output,
)
}
func runWithPrivateRegistryRetry(t *testing.T, cmd icmd.Cmd, opts ...icmd.CmdOp) *icmd.Result {
t.Helper()
deadline := time.Now().Add(90 * time.Second)
for {
result := icmd.RunCmd(cmd, opts...)
output := result.Combined()
if isPrivateRegistryTransient(output) {
if time.Now().Before(deadline) {
t.Logf("waiting for private registry availability: %s", output)
time.Sleep(500 * time.Millisecond)
continue
}
}
return result
}
}
func isPrivateRegistryTransient(output string) bool {
return strings.Contains(output, "lookup privateregistry") ||
strings.Contains(output, "lookup tlsregistry") ||
strings.Contains(output, "lookup registry") ||
strings.Contains(output, "no such host") ||
strings.Contains(output, "server misbehaving") ||
strings.Contains(output, "Temporary failure in name resolution") ||
strings.Contains(output, "connection refused") ||
strings.Contains(output, "i/o timeout") ||
strings.Contains(output, "TLS handshake timeout") ||
strings.Contains(output, "context deadline exceeded") ||
strings.Contains(output, "connection reset by peer") ||
strings.Contains(output, "unexpected EOF")
}
-6
View File
@@ -23,12 +23,6 @@ func SetupConfigFile(t *testing.T) fs.Dir {
"auths": {
"registry:5000": {
"auth": "ZWlhaXM6cGFzc3dvcmQK"
},
"privateregistry:5001": {
"auth": "ZTJlOnBhc3N3b3Jk"
},
"tlsregistry:5003": {
"auth": "ZTJlOnBhc3N3b3Jk"
}
}}`), fs.WithDir("trust", fs.WithDir("private")))
return *dir
-3
View File
@@ -12,9 +12,6 @@ RUN apk --no-cache add openssl openssh-client openssh-server shadow && \
useradd --create-home --shell /bin/sh --password $(head -c32 /dev/urandom | base64) penguin && \
usermod -aG docker penguin && \
ssh-keygen -A
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
RUN update-ca-certificates
# workaround: ssh session excludes /usr/local/bin from $PATH
RUN ln -s /usr/local/bin/docker /usr/bin/docker
COPY ./connhelper-ssh/entrypoint.sh /
-6
View File
@@ -1,6 +0,0 @@
ARG ENGINE_VERSION
FROM docker:${ENGINE_VERSION}-dind
# Trust the tlsregistry CA so dockerd connects without --insecure-registry.
COPY registry/certs/ca.crt /usr/local/share/ca-certificates/tlsregistry-ca.crt
RUN update-ca-certificates
-3
View File
@@ -1,3 +0,0 @@
FROM registry:3
COPY auth /auth
COPY certs /certs
-1
View File
@@ -1 +0,0 @@
e2e:$2y$05$DxRBsGSy61vZsBgNVxwUh.UtZmlg3wZHMxYcHYAlupY7r1xbIiuoq
-33
View File
@@ -1,33 +0,0 @@
#!/bin/sh
set -eu
# Regenerate test certificates for the TLS-enabled private registry.
# Run this from the repository root or from e2e/testdata/registry/certs/.
cd "$(dirname "$0")"
# --- CA ---
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 \
-key ca.key \
-subj '/CN=Test CA (TLS Registry)' \
-out ca.crt
# --- Server cert for tlsregistry (signed by CA) ---
cat > openssl-tlsregistry.cnf <<-EOF
[v3_req]
subjectAltName=DNS:tlsregistry
EOF
openssl genrsa -out tlsregistry.key 2048
openssl req -new \
-key tlsregistry.key \
-subj '/CN=tlsregistry' \
-out tlsregistry.csr
openssl x509 -req -days 3650 \
-in tlsregistry.csr \
-CA ca.crt -CAkey ca.key \
-CAcreateserial \
-out tlsregistry.crt \
-extfile openssl-tlsregistry.cnf \
-extensions v3_req
rm -f tlsregistry.csr ca.srl openssl-tlsregistry.cnf
+1 -11
View File
@@ -88,18 +88,8 @@ func Confirm(ctx context.Context, in io.Reader, out io.Writer, message string) (
_, _ = out.Write([]byte(message))
// On Windows, force the use of the regular OS stdin stream.
//
// StdStreams() may wrap stdin with windowsconsole.NewAnsiReader to
// emulate VT input on consoles that do not support it natively, but
// that wrapper has historically caused interactive prompts to hang
// or behave incorrectly.
//
// See:
// - https://github.com/moby/moby/issues/14336
// - https://github.com/moby/moby/issues/14210
// - https://github.com/moby/moby/pull/17738
if runtime.GOOS == "windows" {
in = os.Stdin
in = streams.NewIn(os.Stdin)
}
result := make(chan bool)
-84
View File
@@ -1,84 +0,0 @@
package opts
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestValidateThrottleBpsDevice(t *testing.T) {
tests := []struct {
doc string
input string
expectedErr string
expectedPath string
expectedRate uint64
}{
{doc: "plain integer", input: "/dev/sda:1000", expectedPath: "/dev/sda", expectedRate: 1000},
{doc: "with unit", input: "/dev/sda:1mb", expectedPath: "/dev/sda", expectedRate: 1048576},
{doc: "zero", input: "/dev/sda:0", expectedPath: "/dev/sda", expectedRate: 0},
{doc: "missing colon", input: "/dev/sda", expectedErr: "bad format: /dev/sda"},
{doc: "empty device", input: ":1mb", expectedErr: "bad format: :1mb"},
{doc: "missing /dev/ prefix", input: "sda:1mb", expectedErr: "bad format for device path: sda:1mb"},
{doc: "non-numeric rate", input: "/dev/sda:foo", expectedErr: "invalid rate for device"},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
v, err := ValidateThrottleBpsDevice(tc.input)
if tc.expectedErr != "" {
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
assert.Check(t, is.Nil(v))
return
}
assert.NilError(t, err)
assert.Check(t, is.Equal(v.Path, tc.expectedPath))
assert.Check(t, is.Equal(v.Rate, tc.expectedRate))
})
}
}
func TestValidateThrottleIOpsDevice(t *testing.T) {
tests := []struct {
doc string
input string
expectedErr string
expectedPath string
expectedRate uint64
}{
{doc: "valid integer", input: "/dev/sda:100", expectedPath: "/dev/sda", expectedRate: 100},
{doc: "fractional rejected", input: "/dev/sda:1.5", expectedErr: "invalid rate for device"},
{doc: "negative rejected", input: "/dev/sda:-5", expectedErr: "invalid rate for device"},
{doc: "unit suffix rejected (iops are integers)", input: "/dev/sda:1mb", expectedErr: "invalid rate for device"},
{doc: "missing /dev/ prefix", input: "sda:100", expectedErr: "bad format for device path: sda:100"},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
v, err := ValidateThrottleIOpsDevice(tc.input)
if tc.expectedErr != "" {
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
assert.Check(t, is.Nil(v))
return
}
assert.NilError(t, err)
assert.Check(t, is.Equal(v.Path, tc.expectedPath))
assert.Check(t, is.Equal(v.Rate, tc.expectedRate))
})
}
}
func TestThrottledeviceOptSetGetList(t *testing.T) {
opt := NewThrottledeviceOpt(ValidateThrottleBpsDevice)
assert.NilError(t, opt.Set("/dev/sda:1mb"))
assert.NilError(t, opt.Set("/dev/sdb:2mb"))
list := opt.GetList()
assert.Assert(t, is.Len(list, 2))
assert.Check(t, is.Equal(list[0].Path, "/dev/sda"))
assert.Check(t, is.Equal(list[0].Rate, uint64(1048576)))
assert.Check(t, is.Equal(list[1].Path, "/dev/sdb"))
assert.Check(t, is.Equal(list[1].Rate, uint64(2097152)))
assert.Check(t, is.ErrorContains(opt.Set("/dev/sdc:bad"), "invalid rate for device"))
assert.Check(t, is.Equal(opt.Type(), "list"))
}
-57
View File
@@ -1,57 +0,0 @@
package opts
import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestValidateWeightDevice(t *testing.T) {
tests := []struct {
doc string
input string
expectedErr string
expectedPath string
expectedWeight uint16
}{
{doc: "valid minimum", input: "/dev/sda:10", expectedPath: "/dev/sda", expectedWeight: 10},
{doc: "valid maximum", input: "/dev/sda:1000", expectedPath: "/dev/sda", expectedWeight: 1000},
{doc: "zero is accepted (unset)", input: "/dev/sda:0", expectedPath: "/dev/sda", expectedWeight: 0},
{doc: "below minimum", input: "/dev/sda:9", expectedErr: "invalid weight for device: /dev/sda:9"},
{doc: "above maximum", input: "/dev/sda:1001", expectedErr: "invalid weight for device: /dev/sda:1001"},
{doc: "overflows uint16", input: "/dev/sda:70000", expectedErr: "invalid weight for device: /dev/sda:70000"},
{doc: "missing colon", input: "/dev/sda", expectedErr: "bad format: /dev/sda"},
{doc: "empty device", input: ":100", expectedErr: "bad format: :100"},
{doc: "missing /dev/ prefix", input: "sda:100", expectedErr: "bad format for device path: sda:100"},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
v, err := ValidateWeightDevice(tc.input)
if tc.expectedErr != "" {
assert.Check(t, is.Error(err, tc.expectedErr))
assert.Check(t, is.Nil(v))
return
}
assert.NilError(t, err)
assert.Check(t, is.Equal(v.Path, tc.expectedPath))
assert.Check(t, is.Equal(v.Weight, tc.expectedWeight))
})
}
}
func TestWeightdeviceOptSetGetList(t *testing.T) {
opt := NewWeightdeviceOpt(ValidateWeightDevice)
assert.NilError(t, opt.Set("/dev/sda:100"))
assert.NilError(t, opt.Set("/dev/sdb:200"))
list := opt.GetList()
assert.Assert(t, is.Len(list, 2))
assert.Check(t, is.Equal(list[0].Path, "/dev/sda"))
assert.Check(t, is.Equal(list[0].Weight, uint16(100)))
assert.Check(t, is.Equal(list[1].Path, "/dev/sdb"))
assert.Check(t, is.Equal(list[1].Weight, uint16(200)))
assert.Check(t, is.Error(opt.Set("/dev/sdc:1"), "invalid weight for device: /dev/sdc:1"))
assert.Check(t, is.Equal(opt.Type(), "list"))
}
-47
View File
@@ -26,55 +26,8 @@ setup() {
export TEST_CONNHELPER_SSH_ID_RSA_PUB
file="${file}:./e2e/compose-env.connhelper-ssh.yaml"
fi
# Generate TLS certificates for the TLS-enabled private registry.
# The certs are baked into the tlsregistry and engine container images,
# so they must exist on disk before docker compose up --build.
# gen-certs.sh handles its own directory navigation.
certdir=e2e/testdata/registry/certs
missing=0
for f in ca.crt ca.key tlsregistry.crt tlsregistry.key; do
if [ ! -f "${certdir}/${f}" ]; then
missing=1
break
fi
done
if [ "$missing" -eq 1 ]; then
sh e2e/testdata/registry/certs/gen-certs.sh
fi
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose up --build -d >&2
# Ensure supporting services exist before running tests. If one fails to start,
# fail fast and surface logs instead of waiting on downstream DNS timeouts.
local deadline=$((SECONDS + 120))
while [ $SECONDS -lt $deadline ]; do
local ok=1
for svc in registry privateregistry tlsregistry engine; do
cid="$(COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose ps -q "$svc" 2>/dev/null || true)"
if [ -z "$cid" ]; then
ok=0
break
fi
if ! docker inspect -f '{{.State.Running}}' "$cid" 2>/dev/null | grep -q true; then
ok=0
break
fi
done
if [ "$ok" -eq 1 ]; then
break
fi
sleep 1
done
if [ $SECONDS -ge $deadline ]; then
echo "Timed out waiting for e2e services to start" >&2
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose ps >&2 || true
for svc in registry privateregistry tlsregistry engine; do
echo "--- logs: $svc ---" >&2
COMPOSE_PROJECT_NAME=$project COMPOSE_FILE=$file docker compose logs --no-color --tail=200 "$svc" >&2 || true
done
exit 1
fi
local network="${project}_default"
# TODO: only run if inside a container
docker network connect "$network" "$(hostname)"
+36 -34
View File
@@ -15,13 +15,13 @@ require (
dario.cat/mergo v1.0.2
github.com/containerd/errdefs v1.0.0
github.com/containerd/log v0.1.0
github.com/containerd/platforms v1.0.0-rc.5
github.com/containerd/platforms v1.0.0-rc.4
github.com/creack/pty v1.1.24
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.11.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker-credential-helpers v0.9.8
github.com/docker/go-connections v0.8.1
github.com/docker/go-connections v0.7.0
github.com/docker/go-units v0.5.0
github.com/fvbommel/sortorder v1.1.0
github.com/go-jose/go-jose/v4 v4.1.4
@@ -30,10 +30,10 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.28
github.com/moby/go-archive v0.3.3
github.com/mattn/go-runewidth v0.0.24
github.com/moby/go-archive v0.2.0
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.1
github.com/moby/moby/client v0.5.0
github.com/moby/patternmatcher v0.6.1
github.com/moby/swarmkit/v2 v2.1.2
github.com/moby/sys/atomicwriter v0.1.0
@@ -46,24 +46,24 @@ require (
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/sirupsen/logrus v1.10.2
github.com/sirupsen/logrus v1.9.4
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
go.opentelemetry.io/otel v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0
go.opentelemetry.io/otel/metric v1.46.0
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/sdk/metric v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
github.com/xeipuuv/gojsonschema v1.2.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/term v0.44.0
golang.org/x/text v0.38.0
gotest.tools/v3 v3.5.2
tags.cncf.io/container-device-interface v1.1.0
)
@@ -78,34 +78,36 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 // indirect
github.com/docker/go-metrics v0.1.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.7 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/user v0.4.1 // indirect
github.com/moby/sys/userns v0.2.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/net v0.58.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect
google.golang.org/grpc v1.83.2 // indirect
google.golang.org/protobuf v1.36.12 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+142 -78
View File
@@ -6,6 +6,10 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -20,78 +24,99 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU=
github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0Q5b3op97T4=
github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE=
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M=
github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 h1:Bj+mzWc7MJqqD0UzTaPmwszW3ttOVjSFi84ZU5l+2I0=
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
github.com/docker/go-metrics v0.1.0 h1:r76KPNpstz+IvQKSWpYegSkkyzex0V3A1ZGVx6bhGlY=
github.com/docker/go-metrics v0.1.0/go.mod h1:PciI3sONtB051kXALN1JoIlpcu54E1FuPh+4DuqEzyw=
github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
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/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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.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.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
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.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.3.3 h1:OxxR9paxsluYi+zDUEXTTaIxtkK3viymW+Ka7vRhhME=
github.com/moby/go-archive v0.3.3/go.mod h1:Npdv43fFqlhZW7Xo8fbm3ZMYFvAGNviUPqX21VERbcE=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/swarmkit/v2 v2.1.2 h1:1WDZAI6HVYNKdCG4zlXnTAPyLsLwuhRGWlHoOUf5Z6I=
@@ -100,10 +125,6 @@ github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I=
github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00=
github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8=
github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o=
github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
@@ -112,107 +133,144 @@ github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrN
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
github.com/moby/sys/userns v0.2.0 h1:nEtDtp7NCV/6dutSklNe8FrENPwFdc4mXnZqC/JWgXM=
github.com/moby/sys/userns v0.2.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
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.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/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_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346 h1:TvtdmeYsYEij78hS4oxnwikoiLdIrgav3BA+CbhaDAI=
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346/go.mod h1:xKQhd7snlzKFuUi1taTGWjpRE8iFTA06DeacYi3CVFQ=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
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.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY=
go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU=
go.opentelemetry.io/otel/metric/x v0.68.0/go.mod h1:agudOmvWhwUTjgibWDzxD2PoWYnpw5Ht5jISYOD2Hd4=
go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190801041406-cbf593c0f2f3/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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -225,15 +283,21 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 h1:izFU9hz7aeLI/Mi1J0991ae+xcwRLr7hTqWnB/9aIIU=
google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5/go.mod h1:3LhxRw4YYkf+ylAfgaY9JlVLFKhokkCV8duhLLe7+t0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 h1:1VUiZAXyC+zmiFYi+WLtBzr68Cj8wOofHjjrA/kkizc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
+9 -16
View File
@@ -87,26 +87,19 @@ func checkWindowsHostAndContainerCompat(host, ctr windowsOSVersion) bool {
return host.Build == ctr.Build
}
// Find the floor of the compatible container range. Per the Windows stable
// ABI policy, every host from LTSC N up to (but not including) LTSC N+1 can
// run containers from LTSC N-1 up to the host build.
// Find the latest LTSC version that is earlier than the host version.
// This is the earliest version of container that the host can run.
//
// So we find the largest LTSC <= host.Build, then step one entry back to
// get the floor. If host.Build is past the latest LTSC in the list
// (e.g. a 26200 host, which is in the WS2025 generation), the floor is
// still the previous LTSC (20348), not the latest LTSC itself.
// If the host version is an LTSC, then it supports compatibility with
// everything from the previous LTSC up to itself, so we want supportedLTSCRelease
// to be the previous entry.
//
// If host is the very first LTSC (or no entry matches, which is impossible
// here since we already checked host.Build >= ltsc2022), use that LTSC as
// the floor.
// If no match is found, then we know that the host is LTSC2022 exactly,
// since we already checked that it's not less than LTSC2022.
var supportedLTSCRelease uint16 = ltsc2022
for i := len(compatLTSCReleases) - 1; i >= 0; i-- {
if host.Build >= compatLTSCReleases[i] {
if i == 0 {
supportedLTSCRelease = compatLTSCReleases[i]
} else {
supportedLTSCRelease = compatLTSCReleases[i-1]
}
if host.Build > compatLTSCReleases[i] {
supportedLTSCRelease = compatLTSCReleases[i]
break
}
}
+13 -37
View File
@@ -1,19 +1,21 @@
package sockets
import (
"context"
"net"
"sync"
)
// inmemAddr is used to satisfy net.Addr for the in-memory socket.
type inmemAddr string
// dummyAddr is used to satisfy net.Addr for the in-mem socket
// it is just stored as a string and returns the string for all calls
type dummyAddr string
// Network returns the addr string, satisfies net.Addr
func (a inmemAddr) Network() string { return "inmem" }
func (a dummyAddr) Network() string {
return string(a)
}
// String returns the string form
func (a inmemAddr) String() string {
func (a dummyAddr) String() string {
return string(a)
}
@@ -21,7 +23,7 @@ func (a inmemAddr) String() string {
type InmemSocket struct {
chConn chan net.Conn
chClose chan struct{}
addr inmemAddr
addr dummyAddr
mu sync.Mutex
}
@@ -32,7 +34,7 @@ func NewInmemSocket(addr string, bufSize int) *InmemSocket {
return &InmemSocket{
chConn: make(chan net.Conn, bufSize),
chClose: make(chan struct{}),
addr: inmemAddr(addr),
addr: dummyAddr(addr),
}
}
@@ -65,41 +67,15 @@ func (s *InmemSocket) Close() error {
return nil
}
// Dial establishes a connection with the in-memory listener.
//
// The network and addr parameters are accepted for compatibility with
// conventional dialer APIs but are currently ignored.
//
// It is equivalent to calling DialContext with context.Background().
// It returns [net.ErrClosed] if the listener has already been closed.
// Dial is used to establish a connection with the in-mem server.
// It returns a [net.ErrClosed] if the connection is already closed.
func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) {
return s.DialContext(context.Background(), network, addr)
}
// DialContext establishes a connection with the in-memory listener.
//
// The network and addr parameters are accepted for compatibility with
// conventional dialer APIs but are currently ignored.
//
// If ctx is canceled before the connection is established, DialContext
// returns the context error. It returns [net.ErrClosed] if the listener
// has already been closed.
func (s *InmemSocket) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
srvConn, clientConn := net.Pipe()
select {
case s.chConn <- srvConn:
return clientConn, nil
case <-ctx.Done():
_ = srvConn.Close()
_ = clientConn.Close()
return nil, ctx.Err()
case <-s.chClose:
_ = srvConn.Close()
_ = clientConn.Close()
return nil, net.ErrClosed
}
return clientConn, nil
}
+19 -50
View File
@@ -47,69 +47,38 @@ For example:
package sockets
import (
"errors"
"fmt"
"net"
"os"
"runtime"
"syscall"
)
const supportsAbstractSockets = runtime.GOOS == "linux"
// SockOption sets up socket file's creating option
type SockOption func(string) error
// NewUnixSocketWithOpts creates a Unix socket with the specified options.
// NewUnixSocketWithOpts creates a unix socket with the specified options.
// By default, socket permissions are 0000 (i.e.: no access for anyone); pass
// WithChmod() and WithChown() to set the desired ownership and permissions.
//
// On Unix platforms, socket permissions are 0000 by default, i.e. no access
// for anyone. Pass WithChmod() and WithChown() to set the desired permissions
// and ownership.
//
// On Windows, the socket uses Windows ACLs. Pass WithBasePermissions() to allow
// Administrators and LocalSystem full access, or WithAdditionalUsersAndGroups()
// to also grant generic read and write access to additional users or groups.
//
// Abstract Unix sockets (Go's Linux-specific "@" shorthand and the native
// leading-NUL representation) are supported only on Linux. On other platforms,
// attempts to use abstract socket addresses return an error. Because abstract
// sockets have no filesystem representation, filesystem-specific socket
// options are not supported.
//
// On platforms without abstract Unix socket support, attempts to use abstract
// socket addresses return an error wrapping [errors.ErrUnsupported].
// This function temporarily changes the system's "umask" to 0777 to work around
// a race condition between creating the socket and setting its permissions. While
// this should only be for a short duration, it may affect other processes that
// create files/directories during that period.
func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) {
if isAbstractSocket(path) {
if !supportsAbstractSockets {
return nil, fmt.Errorf("abstract Unix socket %q is not supported on %s: %w", path, runtime.GOOS, errors.ErrUnsupported)
}
for _, opt := range opts {
if err := opt(path); err != nil {
return nil, err
}
}
return net.Listen("unix", path)
}
if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) {
return nil, err
}
return listenUnix(path, opts...)
}
l, err := listenUnix(path)
if err != nil {
return nil, err
}
// isAbstractSocket reports whether path is an abstract Unix socket address.
//
// Go recognizes two representations of abstract socket addresses:
//
// - On Linux, a path beginning with '@' is translated by the standard library
// to the kernel's native leading-NUL representation.
// See https://pkg.go.dev/net@go1.27rc2#UnixAddr.
//
// - A path beginning with a NUL byte uses the kernel's native representation
// directly. See https://github.com/golang/go/issues/78615.
//
// The interpretation of these addresses is platform-dependent; this helper only
// recognizes the syntax.
func isAbstractSocket(path string) bool {
return len(path) > 0 && (path[0] == '@' || path[0] == 0)
for _, op := range opts {
if err := op(path); err != nil {
_ = l.Close()
return nil, err
}
}
return l, nil
}
-39
View File
@@ -1,39 +0,0 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
package sockets
import (
"runtime"
"syscall"
)
// maxListenerBacklog is similar to the equivalent in stdlib;
// https://github.com/golang/go/blob/go1.26.3/src/net/sock_bsd.go#L14-L39
func maxListenerBacklog() int {
var (
n uint32
err error
)
switch runtime.GOOS {
case "darwin", "ios":
n, err = syscall.SysctlUint32("kern.ipc.somaxconn")
case "freebsd":
n, err = syscall.SysctlUint32("kern.ipc.soacceptqueue")
case "netbsd":
// NOTE: NetBSD has no somaxconn-like kernel state so far
case "openbsd":
n, err = syscall.SysctlUint32("kern.somaxconn")
default:
return syscall.SOMAXCONN
}
if n == 0 || err != nil {
return syscall.SOMAXCONN
}
// FreeBSD stores the backlog in a uint16, as does Linux.
// Assume the other BSDs do too. Truncate number to avoid wrapping.
// See issue 5030.
if n > 1<<16-1 {
n = 1<<16 - 1
}
return int(n)
}
-25
View File
@@ -1,25 +0,0 @@
package sockets
import (
"os"
"strconv"
"strings"
"syscall"
)
// maxListenerBacklog returns the maximum length of the queue of pending
// connections for a listening socket.
//
// It is similar to in stdlib, but without the fallbacks for Kernel < 4.1.0;
// https://github.com/golang/go/blob/go1.26.3/src/net/sock_linux.go#L33-L53
func maxListenerBacklog() int {
b, err := os.ReadFile("/proc/sys/net/core/somaxconn")
if err != nil {
return syscall.SOMAXCONN
}
n, err := strconv.Atoi(strings.TrimSpace(string(b)))
if err != nil || n <= 0 {
return syscall.SOMAXCONN
}
return n
}
-9
View File
@@ -1,9 +0,0 @@
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !windows
package sockets
import "syscall"
func maxListenerBacklog() int {
return syscall.SOMAXCONN
}
+18 -113
View File
@@ -3,35 +3,14 @@
package sockets
import (
"errors"
"fmt"
"net"
"os"
"sync"
"syscall"
)
// defaultSocketPerms is the default permission mode applied to newly created
// Unix sockets. Sockets are created inaccessible by default; callers can
// override this by passing [WithChmod].
//
// TODO(thaJeztah): Consider changing the default to 0o600, making the socket usable by its owner by default.
const defaultSocketPerms os.FileMode = 0o000
// WithChown modifies the socket file's uid and gid.
//
// Abstract Unix sockets have no filesystem representation, so this option
// returns an error wrapping [errors.ErrUnsupported] when used with an abstract
// socket.
// WithChown modifies the socket file's uid and gid
func WithChown(uid, gid int) SockOption {
return func(path string) error {
if isAbstractSocket(path) {
return &os.PathError{
Op: "chown",
Path: path,
Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported),
}
}
if err := os.Chown(path, uid, gid); err != nil {
return err
}
@@ -40,19 +19,8 @@ func WithChown(uid, gid int) SockOption {
}
// WithChmod modifies socket file's access mode.
//
// Abstract Unix sockets have no filesystem representation, so this option
// returns an error wrapping [errors.ErrUnsupported] when used with an abstract
// socket.
func WithChmod(mask os.FileMode) SockOption {
return func(path string) error {
if isAbstractSocket(path) {
return &os.PathError{
Op: "chmod",
Path: path,
Err: fmt.Errorf("abstract Unix sockets do not support filesystem permissions: %w", errors.ErrUnsupported),
}
}
if err := os.Chmod(path, mask); err != nil {
return err
}
@@ -60,90 +28,27 @@ func WithChmod(mask os.FileMode) SockOption {
}
}
// NewUnixSocket creates a Unix socket with the specified path and group.
//
// On Unix platforms, the socket is owned by root:gid and has permissions 0660.
//
// Abstract Unix sockets are not supported by this helper. Use [NewUnixSocketWithOpts]
// without filesystem permission options instead.
// NewUnixSocket creates a unix socket with the specified path and group.
func NewUnixSocket(path string, gid int) (net.Listener, error) {
return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660))
}
func listenUnix(path string, opts ...SockOption) (_ net.Listener, retErr error) {
// net.Listen does not allow permissions or ownership to be set between
// bind(2), which creates the socket path, and listen(2), which makes it
// possible for clients to connect.
func listenUnix(path string) (net.Listener, error) {
// net.Listen does not allow for permissions to be set. As a result, when
// specifying custom permissions ("WithChmod()"), there is a short time
// between creating the socket and applying the permissions, during which
// the socket permissions are Less restrictive than desired.
//
// Creating the socket manually lets us apply options after bind(2), but
// before listen(2). This avoids temporarily relaxing the process umask while
// still preventing a socket from becoming connectable before the requested
// permissions are applied.
// To work around this limitation of net.Listen(), we temporarily set the
// umask to 0777, which forces the socket to be created with 000 permissions
// (i.e.: no access for anyone). After that, WithChmod() must be used to set
// the desired permissions.
//
// See https://github.com/golang/go/issues/11822
// Similar to sysSocket in stdlib, but without the fast path for Linux.
// https://github.com/golang/go/blob/go1.26.3/src/net/sys_cloexec.go#L18-L36
syscall.ForkLock.RLock()
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
if err == nil {
syscall.CloseOnExec(fd) // No syscall.SOCK_CLOEXEC on macOS.
}
syscall.ForkLock.RUnlock()
if err != nil {
return nil, os.NewSyscallError("socket", err)
}
defer func() {
if fd >= 0 {
_ = syscall.Close(fd)
}
}()
if err := syscall.Bind(fd, &syscall.SockaddrUnix{Name: path}); err != nil {
return nil, os.NewSyscallError("bind", err)
}
defer func() {
if retErr != nil {
_ = syscall.Unlink(path)
}
}()
// Secure by default: the socket is not accessible at all
// unless permission options are set through WithChmod.
if err := os.Chmod(path, defaultSocketPerms); err != nil {
return nil, err
}
for _, op := range opts {
if err := op(path); err != nil {
return nil, err
}
}
if err := syscall.Listen(fd, listenerBacklog()); err != nil {
return nil, os.NewSyscallError("listen", err)
}
f := os.NewFile(uintptr(fd), "unix:"+path)
fd = -1 // f now owns the original fd; prevent the defer from closing it.
// FileListener duplicates f, sets the duplicate close-on-exec and nonblocking,
// and returns a net.Listener backed by that duplicate. The temporary *os.File
// is no longer needed after this point.
l, err := net.FileListener(f)
_ = f.Close()
if err != nil {
return nil, err
}
if ul, ok := l.(*net.UnixListener); ok {
ul.SetUnlinkOnClose(true)
}
return l, nil
// We don't use "defer" here, to reset the umask to its original value as soon
// as possible. Ideally we'd be able to detect if WithChmod() was passed as
// an option, and skip changing umask if default permissions are used.
origUmask := syscall.Umask(0o777)
l, err := net.Listen("unix", path)
syscall.Umask(origUmask)
return l, err
}
// listenerBacklog is a caching wrapper around maxListenerBacklog.
var listenerBacklog = sync.OnceValue(maxListenerBacklog)
+10 -24
View File
@@ -48,7 +48,7 @@ func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption
}
sd, err := getSecurityDescriptor(additionalUsersAndGroups...)
if err != nil {
return err
return fmt.Errorf("looking up SID: %w", err)
}
return withSDDL(sd)(path)
}
@@ -85,15 +85,12 @@ func withSDDL(sddl string) SockOption {
}
}
// NewUnixSocket creates a new Unix socket.
// NewUnixSocket creates a new unix socket.
//
// It sets [BasePermissions] on the socket path and grants the given additional
// users and groups to generic read (GR) and write (GW) access. It returns
// an error when failing to resolve any of the additional users and groups,
// or when failing to apply the ACL.
//
// Abstract Unix sockets are not supported by this helper. Attempts to use
// abstract socket addresses return an error wrapping [errors.ErrUnsupported].
func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) {
var opts []SockOption
if len(additionalUsersAndGroups) > 0 {
@@ -106,38 +103,27 @@ func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener
// getSecurityDescriptor returns the DACL for the Unix socket.
//
// By default, it grants [BasePermissions]. Additional users and groups
// are granted generic read (GR) and write (GW) access. It returns an
// error if any name cannot be resolved to a SID.
// By default, it grants [BasePermissions], but allows for additional
// users and groups to get generic read (GR) and write (GW) access. It
// returns an error when failing to resolve any of the additional users
// and groups.
func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) {
sddl := BasePermissions
// Grant generic read (GR) and write (GW) access to whatever
// additional users or groups were specified.
//
// We keep duplicates; two identical allow ACEs are redundant,
// but they do not create conflicting permissions, so should not error.
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/20233ed8-a6c6-4097-aafa-dd545ed24428
// TODO(thaJeztah): should we fail on, or remove duplicates?
for _, g := range additionalUsersAndGroups {
sid, err := winio.LookupSidByName(strings.TrimSpace(g))
if err != nil {
return "", fmt.Errorf("looking up SID: %w", err)
}
sddl += "(A;;GRGW;;;" + sid + ")"
sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid)
}
return sddl, nil
}
func listenUnix(path string, opts ...SockOption) (net.Listener, error) {
l, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
for _, op := range opts {
if err := op(path); err != nil {
_ = l.Close()
return nil, err
}
}
return l, nil
func listenUnix(path string) (net.Listener, error) {
return net.Listen("unix", path)
}
+425
View File
@@ -0,0 +1,425 @@
Attribution-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and
conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
l. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
m. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses.
Notwithstanding, Creative Commons may elect to apply one of its public
licenses to material it publishes and in those instances will be
considered the "Licensor." Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the public
licenses.
Creative Commons may be contacted at creativecommons.org.
+32 -42
View File
@@ -1,71 +1,59 @@
# go-metrics
[![Go Reference](https://pkg.go.dev/badge/github.com/docker/go-metrics.svg)](https://pkg.go.dev/github.com/docker/go-metrics)
# go-metrics [![GoDoc](https://godoc.org/github.com/docker/go-metrics?status.svg)](https://godoc.org/github.com/docker/go-metrics) ![Badge Badge](http://doyouevenbadge.com/github.com/docker/go-metrics)
This package is small wrapper around the prometheus go client to help enforce convention and best practices for metrics collection in Docker projects.
## Best Practices
This packages is meant to be used for collecting metrics. It is not meant to be
used as a replacement for the prometheus client but to help enforce consistent
naming across metrics collected. If you have not already read the prometheus
best practices around naming and labels you can read the naming section in the
[prometheus documentation](https://prometheus.io/docs/practices/naming/).
This packages is meant to be used for collecting metrics in Docker projects.
It is not meant to be used as a replacement for the prometheus client but to help enforce consistent naming across metrics collected.
If you have not already read the prometheus best practices around naming and labels you can read the page [here](https://prometheus.io/docs/practices/naming/).
The following are a few conventions that help naming metrics in your project.
The following are a few Docker specific rules that will help you name and work with metrics in your project.
### Namespace and Subsystem
1. Namespace and Subsystem
This package provides you with a namespace type that allows you to specify the
same namespace and subsystem for your metrics.
This package provides you with a namespace type that allows you to specify the same namespace and subsystem for your metrics.
```go
ns := metrics.NewNamespace("engine", "daemon", metrics.Labels{
"version": daemon.Version,
"commit": daemon.GitCommit,
"version": dockerversion.Version,
"commit": dockerversion.GitCommit,
})
```
In the example above we are creating metrics for the daemon package.
`engine` would be the namespace in this example where `daemon` is the subsystem
or package where we are collecting the metrics.
In the example above we are creating metrics for the Docker engine's daemon package.
`engine` would be the namespace in this example where `daemon` is the subsystem or package where we are collecting the metrics.
A namespace also allows you to attach constant labels to the metrics such as
the git commit and version that it is collecting.
A namespace also allows you to attach constant labels to the metrics such as the git commit and version that it is collecting.
### Declaring your Metrics
2. Declaring your Metrics
Try to keep all your metric declarations in one file. This makes it easy for
others to see what constant labels are defined on the namespace and what
labels are defined on the metrics when they are created.
Try to keep all your metric declarations in one file.
This makes it easy for others to see what constant labels are defined on the namespace and what labels are defined on the metrics when they are created.
### Use labels instead of multiple metrics
3. Use labels instead of multiple metrics
Labels allow you to define one metric such as the time it takes to perform a
certain action on an object. If we wanted to collect timings on various container
actions such as create, start, and delete then we can define one metric called
`container_actions` and use labels to specify the type of action.
Labels allow you to define one metric such as the time it takes to perform a certain action on an object.
If we wanted to collect timings on various container actions such as create, start, and delete then we can define one metric called `container_actions` and use labels to specify the type of action.
```go
containerActions = ns.NewLabeledTimer("container_actions", "The number of milliseconds it takes to process each container action", "action")
```
The last parameter is the label name or key. When adding a data point to the
metric you can use the `WithValues` function to specify the `action` that you
are collecting for.
The last parameter is the label name or key.
When adding a data point to the metric you will use the `WithValues` function to specify the `action` that you are collecting for.
```go
containerActions.WithValues("create").UpdateSince(start)
```
### Always use a unit
4. Always use a unit
The metric name should describe what you are measuring but you also need to
provide the unit that it is being measured with. For a timer, the standard
unit is seconds and a counter's standard unit is a total. For gauges you must
provide the unit. This package provides a standard set of units for use within
your projects.
The metric name should describe what you are measuring but you also need to provide the unit that it is being measured with.
For a timer, the standard unit is seconds and a counter's standard unit is a total.
For gauges you must provide the unit.
This package provides a standard set of units for use within the Docker projects.
```go
Nanoseconds Unit = "nanoseconds"
@@ -74,16 +62,18 @@ Bytes Unit = "bytes"
Total Unit = "total"
```
If you need to use a unit but it is not defined in the package please open
a PR to add it but first try to see if one of the existing units work for
your metric, i.e., seconds or nanoseconds vs adding milliseconds.
If you need to use a unit but it is not defined in the package please open a PR to add it but first try to see if one of the already created units will work for your metric, i.e. seconds or nanoseconds vs adding milliseconds.
## Docs
Package documentation can be found [here](https://godoc.org/github.com/docker/go-metrics).
## HTTP Metrics
To instrument a http handler, you can wrap the code like this:
```go
namespace := metrics.NewNamespace("api_server", "http", metrics.Labels{"handler": "your_http_handler_name"})
namespace := metrics.NewNamespace("docker_distribution", "http", metrics.Labels{"handler": "your_http_handler_name"})
httpMetrics := namespace.NewDefaultHttpMetrics()
metrics.Register(namespace)
instrumentedHandler = metrics.InstrumentHandler(httpMetrics, unInstrumentedHandler)
@@ -98,4 +88,4 @@ If you need a custom metrics and it is generic enough to be used by multiple pro
## Copyright and license
Copyright © 2016 Docker, Inc. All rights reserved, except as follows. Code is released under the Apache 2.0 license.
Copyright © 2016 Docker, Inc. All rights reserved, except as follows. Code is released under the Apache 2.0 license. The README.md file, and files in the "docs" folder are licensed under the Creative Commons Attribution 4.0 International License under the terms and conditions set forth in the file "LICENSE.docs". You may obtain a duplicate copy of the same license, titled CC-BY-SA-4.0, at http://creativecommons.org/licenses/by/4.0/.
-4
View File
@@ -23,12 +23,10 @@ func (lc *labeledCounter) WithValues(vs ...string) Counter {
return &counter{pc: lc.pc.WithLabelValues(vs...)}
}
// Describe implements [prometheus.Collector].
func (lc *labeledCounter) Describe(ch chan<- *prometheus.Desc) {
lc.pc.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (lc *labeledCounter) Collect(ch chan<- prometheus.Metric) {
lc.pc.Collect(ch)
}
@@ -45,12 +43,10 @@ func (c *counter) Inc(vs ...float64) {
c.pc.Add(sumFloat64(vs...))
}
// Describe implements [prometheus.Collector].
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
c.pc.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (c *counter) Collect(ch chan<- prometheus.Metric) {
c.pc.Collect(ch)
}
+1 -2
View File
@@ -1,4 +1,3 @@
// Package metrics is small wrapper around the prometheus go client to help
// enforce convention and best practices for metrics collection.
// This package is small wrapper around the prometheus go client to help enforce convention and best practices for metrics collection in Docker projects.
package metrics
-4
View File
@@ -27,12 +27,10 @@ func (lg *labeledGauge) WithValues(labels ...string) Gauge {
return &gauge{pg: lg.pg.WithLabelValues(labels...)}
}
// Describe implements [prometheus.Collector].
func (lg *labeledGauge) Describe(c chan<- *prometheus.Desc) {
lg.pg.Describe(c)
}
// Collect implements [prometheus.Collector].
func (lg *labeledGauge) Collect(c chan<- prometheus.Metric) {
lg.pg.Collect(c)
}
@@ -65,12 +63,10 @@ func (g *gauge) Set(v float64) {
g.pg.Set(v)
}
// Describe implements [prometheus.Collector].
func (g *gauge) Describe(c chan<- *prometheus.Desc) {
g.pg.Describe(c)
}
// Collect implements [prometheus.Collector].
func (g *gauge) Collect(c chan<- prometheus.Metric) {
g.pg.Collect(c)
}
+39 -33
View File
@@ -7,62 +7,68 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// HTTPHandlerOpts configures HTTP instrumentation metrics.
// HTTPHandlerOpts describes a set of configurable options of http metrics
type HTTPHandlerOpts struct {
DurationBuckets []float64
RequestSizeBuckets []float64
ResponseSizeBuckets []float64
}
// HTTPMetric describes a metric used to instrument an HTTP handler.
//
// HTTPMetric values must be created using the HTTP metric methods on
// [Namespace], such as [Namespace.NewDefaultHttpMetrics] or
// [Namespace.NewHttpMetricsWithOpts].
const (
InstrumentHandlerResponseSize = iota
InstrumentHandlerRequestSize
InstrumentHandlerDuration
InstrumentHandlerCounter
InstrumentHandlerInFlight
)
type HTTPMetric struct {
collector prometheus.Collector
wrap func(http.Handler) http.Handler
}
var _ prometheus.Collector = (*HTTPMetric)(nil)
// Describe implements [prometheus.Collector].
func (m *HTTPMetric) Describe(ch chan<- *prometheus.Desc) {
m.collector.Describe(ch)
}
// Collect implements [prometheus.Collector].
func (m *HTTPMetric) Collect(ch chan<- prometheus.Metric) {
m.collector.Collect(ch)
prometheus.Collector
handlerType int
}
var (
defaultDurationBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 60}
defaultRequestSizeBuckets = prometheus.ExponentialBuckets(1024, 2, 22) // 1K to 4G
defaultRequestSizeBuckets = prometheus.ExponentialBuckets(1024, 2, 22) //1K to 4G
defaultResponseSizeBuckets = defaultRequestSizeBuckets
)
// Handler is a convenience wrapper around [promhttp.Handler] that returns an
// HTTP handler serving metrics from the default Prometheus gatherer.
// Handler returns the global http.Handler that provides the prometheus
// metrics format on GET requests. This handler is no longer instrumented.
func Handler() http.Handler {
return promhttp.Handler()
}
// InstrumentHandler returns an HTTP handler function that instruments handler
// with the provided HTTP metrics.
func InstrumentHandler(metrics []*HTTPMetric, handler http.Handler) http.HandlerFunc {
return instrumentHandler(metrics, handler)
return InstrumentHandlerFunc(metrics, handler.ServeHTTP)
}
// InstrumentHandlerFunc returns an HTTP handler function that instruments
// handlerFunc with the provided HTTP metrics.
func InstrumentHandlerFunc(metrics []*HTTPMetric, handlerFunc http.HandlerFunc) http.HandlerFunc {
return instrumentHandler(metrics, handlerFunc)
}
func instrumentHandler(metrics []*HTTPMetric, handler http.Handler) http.HandlerFunc {
var handler http.Handler
handler = http.HandlerFunc(handlerFunc)
for _, metric := range metrics {
handler = metric.wrap(handler)
switch metric.handlerType {
case InstrumentHandlerResponseSize:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerResponseSize(collector, handler)
}
case InstrumentHandlerRequestSize:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerRequestSize(collector, handler)
}
case InstrumentHandlerDuration:
if collector, ok := metric.Collector.(prometheus.ObserverVec); ok {
handler = promhttp.InstrumentHandlerDuration(collector, handler)
}
case InstrumentHandlerCounter:
if collector, ok := metric.Collector.(*prometheus.CounterVec); ok {
handler = promhttp.InstrumentHandlerCounter(collector, handler)
}
case InstrumentHandlerInFlight:
if collector, ok := metric.Collector.(prometheus.Gauge); ok {
handler = promhttp.InstrumentHandlerInFlight(collector, handler)
}
}
}
return handler.ServeHTTP
}
+85 -129
View File
@@ -1,25 +1,27 @@
package metrics
import (
"maps"
"net/http"
"fmt"
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// Labels represents a collection of metric label names and values.
type Labels = prometheus.Labels
type Labels map[string]string
// NewNamespace returns a namespace for metrics that share the given namespace
// and subsystem. labels are added as constant labels to metrics created by the
// namespace.
// NewNamespace returns a namespaces that is responsible for managing a collection of
// metrics for a particual namespace and subsystem
//
// labels allows const labels to be added to all metrics created in this namespace
// and are commonly used for data like application version and git commit
func NewNamespace(name, subsystem string, labels Labels) *Namespace {
if labels == nil {
labels = make(map[string]string)
}
return &Namespace{
name: name,
subsystem: subsystem,
labels: maps.Clone(labels),
labels: labels,
}
}
@@ -27,7 +29,7 @@ func NewNamespace(name, subsystem string, labels Labels) *Namespace {
type Namespace struct {
name string
subsystem string
labels prometheus.Labels
labels Labels
mu sync.Mutex
metrics []prometheus.Collector
}
@@ -35,8 +37,8 @@ type Namespace struct {
// WithConstLabels returns a namespace with the provided set of labels merged
// with the existing constant labels on the namespace.
//
// Only metrics created with the returned namespace will get the new constant
// labels. The returned namespace must be registered separately.
// Only metrics created with the returned namespace will get the new constant
// labels. The returned namespace must be registered separately.
func (n *Namespace) WithConstLabels(labels Labels) *Namespace {
n.mu.Lock()
ns := &Namespace{
@@ -48,15 +50,12 @@ func (n *Namespace) WithConstLabels(labels Labels) *Namespace {
return ns
}
// NewCounter creates and adds a counter to the namespace.
func (n *Namespace) NewCounter(name, help string) Counter {
c := &counter{pc: prometheus.NewCounter(n.newCounterOpts(name, help))}
n.Add(c)
return c
}
// NewLabeledCounter creates and adds a counter with the given variable labels
// to the namespace.
func (n *Namespace) NewLabeledCounter(name, help string, labels ...string) LabeledCounter {
c := &labeledCounter{pc: prometheus.NewCounterVec(n.newCounterOpts(name, help), labels)}
n.Add(c)
@@ -69,64 +68,36 @@ func (n *Namespace) newCounterOpts(name, help string) prometheus.CounterOpts {
Subsystem: n.subsystem,
Name: makeName(name, Total),
Help: help,
ConstLabels: n.labels,
ConstLabels: prometheus.Labels(n.labels),
}
}
// NewTimer creates and adds a timer to the namespace.
func (n *Namespace) NewTimer(name, help string) Timer {
t := &timer{
m: prometheus.NewHistogram(n.newTimerOpts(name, help, []float64{})),
m: prometheus.NewHistogram(n.newTimerOpts(name, help)),
}
n.Add(t)
return t
}
// NewTimerWithBuckets creates and adds a timer with the given histogram buckets
// to the namespace.
func (n *Namespace) NewTimerWithBuckets(name, help string, buckets []float64) Timer {
t := &timer{
m: prometheus.NewHistogram(n.newTimerOpts(name, help, buckets)),
}
n.Add(t)
return t
}
// NewLabeledTimer creates and adds a timer with the given variable labels to
// the namespace.
func (n *Namespace) NewLabeledTimer(name, help string, labels ...string) LabeledTimer {
t := &labeledTimer{
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help, []float64{}), labels),
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help), labels),
}
n.Add(t)
return t
}
// NewLabeledTimerWithBuckets creates and adds a timer with the given histogram
// buckets and variable labels to the namespace.
func (n *Namespace) NewLabeledTimerWithBuckets(name, help string, buckets []float64, labels ...string) LabeledTimer {
t := &labeledTimer{
m: prometheus.NewHistogramVec(n.newTimerOpts(name, help, buckets), labels),
}
n.Add(t)
return t
}
func (n *Namespace) newTimerOpts(name, help string, buckets []float64) prometheus.HistogramOpts {
opts := prometheus.HistogramOpts{
func (n *Namespace) newTimerOpts(name, help string) prometheus.HistogramOpts {
return prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: makeName(name, Seconds),
Help: help,
ConstLabels: n.labels,
ConstLabels: prometheus.Labels(n.labels),
}
if len(buckets) > 0 {
opts.Buckets = buckets
}
return opts
}
// NewGauge creates and adds a gauge with the given unit to the namespace.
func (n *Namespace) NewGauge(name, help string, unit Unit) Gauge {
g := &gauge{
pg: prometheus.NewGauge(n.newGaugeOpts(name, help, unit)),
@@ -135,8 +106,6 @@ func (n *Namespace) NewGauge(name, help string, unit Unit) Gauge {
return g
}
// NewLabeledGauge creates and adds a gauge with the given unit and variable
// labels to the namespace.
func (n *Namespace) NewLabeledGauge(name, help string, unit Unit, labels ...string) LabeledGauge {
g := &labeledGauge{
pg: prometheus.NewGaugeVec(n.newGaugeOpts(name, help, unit), labels),
@@ -151,13 +120,10 @@ func (n *Namespace) newGaugeOpts(name, help string, unit Unit) prometheus.GaugeO
Subsystem: n.subsystem,
Name: makeName(name, unit),
Help: help,
ConstLabels: n.labels,
ConstLabels: prometheus.Labels(n.labels),
}
}
// Describe sends the descriptions of all metrics in the namespace to ch.
//
// Describe implements [prometheus.Collector].
func (n *Namespace) Describe(ch chan<- *prometheus.Desc) {
n.mu.Lock()
defer n.mu.Unlock()
@@ -167,9 +133,6 @@ func (n *Namespace) Describe(ch chan<- *prometheus.Desc) {
}
}
// Collect sends all metrics in the namespace to ch.
//
// Collect implements [prometheus.Collector].
func (n *Namespace) Collect(ch chan<- prometheus.Metric) {
n.mu.Lock()
defer n.mu.Unlock()
@@ -179,44 +142,33 @@ func (n *Namespace) Collect(ch chan<- prometheus.Metric) {
}
}
// Add adds collector to the namespace.
func (n *Namespace) Add(collector prometheus.Collector) {
n.mu.Lock()
n.metrics = append(n.metrics, collector)
n.mu.Unlock()
}
// NewDesc returns a Prometheus metric descriptor using the namespace,
// subsystem, constant labels, and given variable labels.
func (n *Namespace) NewDesc(name, help string, unit Unit, labels ...string) *prometheus.Desc {
name = makeName(name, unit)
namespace := n.name
if n.subsystem != "" {
namespace += "_" + n.subsystem
namespace = fmt.Sprintf("%s_%s", namespace, n.subsystem)
}
name = namespace + "_" + name
return prometheus.NewDesc(name, help, labels, n.labels)
name = fmt.Sprintf("%s_%s", namespace, name)
return prometheus.NewDesc(name, help, labels, prometheus.Labels(n.labels))
}
// withHandlerLabel returns a copy of labels with the "handler" label set
// to handlerName.
func withHandlerLabel(labels prometheus.Labels, handlerName string) prometheus.Labels {
out := maps.Clone(labels)
if out == nil {
out = make(prometheus.Labels)
}
out["handler"] = handlerName
return out
}
// mergeLabels merges two or more labels objects into a single map, favoring
// the later labels.
func mergeLabels(lbs ...Labels) Labels {
merged := make(Labels)
// mergeLabels returns a new label map containing base and overrides.
// Labels in overrides take precedence over labels in base.
func mergeLabels(base prometheus.Labels, overrides Labels) prometheus.Labels {
merged := maps.Clone(base)
if merged == nil {
merged = make(prometheus.Labels, len(overrides))
for _, target := range lbs {
for k, v := range target {
merged[k] = v
}
}
maps.Copy(merged, overrides)
return merged
}
@@ -224,11 +176,10 @@ func makeName(name string, unit Unit) string {
if unit == "" {
return name
}
return name + "_" + string(unit)
return fmt.Sprintf("%s_%s", name, unit)
}
// NewDefaultHttpMetrics creates and adds the default set of HTTP metrics for
// handlerName using the default histogram buckets.
func (n *Namespace) NewDefaultHttpMetrics(handlerName string) []*HTTPMetric {
return n.NewHttpMetricsWithOpts(handlerName, HTTPHandlerOpts{
DurationBuckets: defaultDurationBuckets,
@@ -237,8 +188,6 @@ func (n *Namespace) NewDefaultHttpMetrics(handlerName string) []*HTTPMetric {
})
}
// NewHttpMetrics creates and adds the default set of HTTP metrics for
// handlerName using the given histogram buckets.
func (n *Namespace) NewHttpMetrics(handlerName string, durationBuckets, requestSizeBuckets, responseSizeBuckets []float64) []*HTTPMetric {
return n.NewHttpMetricsWithOpts(handlerName, HTTPHandlerOpts{
DurationBuckets: durationBuckets,
@@ -247,8 +196,6 @@ func (n *Namespace) NewHttpMetrics(handlerName string, durationBuckets, requestS
})
}
// NewHttpMetricsWithOpts creates and adds the configured set of HTTP metrics
// for handlerName.
func (n *Namespace) NewHttpMetricsWithOpts(handlerName string, opts HTTPHandlerOpts) []*HTTPMetric {
var httpMetrics []*HTTPMetric
inFlightMetric := n.NewInFlightGaugeMetric(handlerName)
@@ -260,100 +207,109 @@ func (n *Namespace) NewHttpMetricsWithOpts(handlerName string, opts HTTPHandlerO
return httpMetrics
}
func (n *Namespace) newHTTPMetric(collector prometheus.Collector, wrap func(http.Handler) http.Handler) *HTTPMetric {
n.Add(collector)
return &HTTPMetric{collector: collector, wrap: wrap}
}
// NewInFlightGaugeMetric creates and adds a metric that tracks the number of
// in-flight HTTP requests for handlerName.
func (n *Namespace) NewInFlightGaugeMetric(handlerName string) *HTTPMetric {
collector := prometheus.NewGauge(prometheus.GaugeOpts{
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
metric := prometheus.NewGauge(prometheus.GaugeOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "in_flight_requests",
Help: "The in-flight HTTP requests",
ConstLabels: withHandlerLabel(n.labels, handlerName),
})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerInFlight(collector, next)
ConstLabels: prometheus.Labels(labels),
})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerInFlight,
}
n.Add(httpMetric)
return httpMetric
}
// NewRequestTotalMetric creates and adds a metric that counts HTTP requests for
// handlerName by response code and method.
func (n *Namespace) NewRequestTotalMetric(handlerName string) *HTTPMetric {
collector := prometheus.NewCounterVec(
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
metric := prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "requests_total",
Help: "Total number of HTTP requests made.",
ConstLabels: withHandlerLabel(n.labels, handlerName),
ConstLabels: prometheus.Labels(labels),
},
[]string{"code", "method"},
)
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerCounter(collector, next)
})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerCounter,
}
n.Add(httpMetric)
return httpMetric
}
// NewRequestDurationMetric creates and adds a metric that records HTTP request
// durations for handlerName using the given histogram buckets.
func (n *Namespace) NewRequestDurationMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("DurationBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "request_duration_seconds",
Help: "The HTTP request latencies in seconds.",
Buckets: buckets,
ConstLabels: withHandlerLabel(n.labels, handlerName),
ConstLabels: prometheus.Labels(labels),
}
collector := prometheus.NewHistogramVec(opts, []string{"method"})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerDuration(collector, next)
})
metric := prometheus.NewHistogramVec(opts, []string{"method"})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerDuration,
}
n.Add(httpMetric)
return httpMetric
}
// NewRequestSizeMetric creates and adds a metric that records HTTP request
// sizes for handlerName using the given histogram buckets.
func (n *Namespace) NewRequestSizeMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("RequestSizeBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "request_size_bytes",
Help: "The HTTP request sizes in bytes.",
Buckets: buckets,
ConstLabels: withHandlerLabel(n.labels, handlerName),
ConstLabels: prometheus.Labels(labels),
}
collector := prometheus.NewHistogramVec(opts, []string{})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerRequestSize(collector, next)
})
metric := prometheus.NewHistogramVec(opts, []string{})
httpMetric := &HTTPMetric{
Collector: metric,
handlerType: InstrumentHandlerRequestSize,
}
n.Add(httpMetric)
return httpMetric
}
// NewResponseSizeMetric creates and adds a metric that records HTTP response
// sizes for handlerName using the given histogram buckets.
func (n *Namespace) NewResponseSizeMetric(handlerName string, buckets []float64) *HTTPMetric {
if len(buckets) == 0 {
panic("ResponseSizeBuckets must be provided")
}
labels := prometheus.Labels(n.labels)
labels["handler"] = handlerName
opts := prometheus.HistogramOpts{
Namespace: n.name,
Subsystem: n.subsystem,
Name: "response_size_bytes",
Help: "The HTTP response sizes in bytes.",
Buckets: buckets,
ConstLabels: withHandlerLabel(n.labels, handlerName),
ConstLabels: prometheus.Labels(labels),
}
collector := prometheus.NewHistogramVec(opts, []string{})
return n.newHTTPMetric(collector, func(next http.Handler) http.Handler {
return promhttp.InstrumentHandlerResponseSize(collector, next)
})
metrics := prometheus.NewHistogramVec(opts, []string{})
httpMetric := &HTTPMetric{
Collector: metrics,
handlerType: InstrumentHandlerResponseSize,
}
n.Add(httpMetric)
return httpMetric
}
+4 -4
View File
@@ -2,14 +2,14 @@ package metrics
import "github.com/prometheus/client_golang/prometheus"
// Register is a convenience wrapper around [prometheus.MustRegister] that
// registers all metrics in n with the default Prometheus registry.
// Register adds all the metrics in the provided namespace to the global
// metrics registry
func Register(n *Namespace) {
prometheus.MustRegister(n)
}
// Deregister is a convenience wrapper around [prometheus.Unregister] that
// unregisters all metrics in n from the default Prometheus registry.
// Deregister removes all the metrics in the provided namespace from the
// global metrics registry
func Deregister(n *Namespace) {
prometheus.Unregister(n)
}
-4
View File
@@ -51,12 +51,10 @@ func (lt *labeledTimer) WithValues(labels ...string) *labeledTimerObserver {
return &labeledTimerObserver{m: lt.m.WithLabelValues(labels...)}
}
// Describe implements [prometheus.Collector].
func (lt *labeledTimer) Describe(c chan<- *prometheus.Desc) {
lt.m.Describe(c)
}
// Collect implements [prometheus.Collector].
func (lt *labeledTimer) Collect(c chan<- prometheus.Metric) {
lt.m.Collect(c)
}
@@ -73,12 +71,10 @@ func (t *timer) UpdateSince(since time.Time) {
t.m.Observe(time.Since(since).Seconds())
}
// Describe implements [prometheus.Collector].
func (t *timer) Describe(c chan<- *prometheus.Desc) {
c <- t.m.(prometheus.Metric).Desc()
}
// Collect implements [prometheus.Collector].
func (t *timer) Collect(c chan<- prometheus.Metric) {
// Are there any observers that don't implement Collector? It is really
// unclear what the point of the upstream change was, but we'll let this
+1 -36
View File
@@ -69,16 +69,6 @@ func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWri
}
},
WriteString: func(next WriteStringFunc) WriteStringFunc {
return func(s string) (int, error) {
n, err := next(s)
m.Written += int64(n)
headerWritten = true
return n, err
}
},
ReadFrom: func(next ReadFromFunc) ReadFromFunc {
return func(src io.Reader) (int64, error) {
n, err := next(src)
@@ -91,31 +81,6 @@ func (m *Metrics) CaptureMetrics(w http.ResponseWriter, fn func(http.ResponseWri
}
)
// defer to ensure duration is updated even if the handler panics
defer func() {
m.Duration += time.Since(start)
}()
fn(Wrap(w, hooks))
}
// deadliner defines two methods introduced in go 1.20. The standard library
// seems not to provide an interface we can import, hence its definition here.
type deadliner interface {
SetReadDeadline(deadline time.Time) error
SetWriteDeadline(deadline time.Time) error
}
// fullDuplexEnabler defines a method introduced in go 1.21. The standard
// library seems not to provide an interface we can import, hence its definition
// here.
type fullDuplexEnabler interface {
EnableFullDuplex() error
}
// httpFlushError defines a method introduced in go 1.20. The standard
// library seems not to provide an interface we can import, hence its definition
// here.
// See https://github.com/golang/go/blob/go1.20/src/net/http/responsecontroller.go#L50
type httpFlushError interface {
FlushError() error
m.Duration += time.Since(start)
}
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
// +build go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// PushFunc is part of the http.Pusher interface.
type PushFunc func(target string, opts *http.PushOptions) error
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
Push func(PushFunc) PushFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
// - http.Pusher
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
_, i4 := w.(http.Pusher)
switch {
// combination 1/32
case !i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/32
case !i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Pusher
}{rw, rw, rw}
// combination 3/32
case !i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 4/32
case !i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw}
// combination 5/32
case !i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 6/32
case !i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
http.Pusher
}{rw, rw, rw, rw}
// combination 7/32
case !i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 8/32
case !i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 9/32
case !i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 10/32
case !i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw}
// combination 11/32
case !i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 12/32
case !i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 13/32
case !i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 14/32
case !i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 15/32
case !i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 16/32
case !i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 17/32
case i0 && !i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 18/32
case i0 && !i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Pusher
}{rw, rw, rw, rw}
// combination 19/32
case i0 && !i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 20/32
case i0 && !i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 21/32
case i0 && !i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 22/32
case i0 && !i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 23/32
case i0 && !i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 24/32
case i0 && !i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 25/32
case i0 && i1 && !i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 26/32
case i0 && i1 && !i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Pusher
}{rw, rw, rw, rw, rw}
// combination 27/32
case i0 && i1 && !i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 28/32
case i0 && i1 && !i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 29/32
case i0 && i1 && i2 && !i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 30/32
case i0 && i1 && i2 && !i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
http.Pusher
}{rw, rw, rw, rw, rw, rw}
// combination 31/32
case i0 && i1 && i2 && i3 && !i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
// combination 32/32
case i0 && i1 && i2 && i3 && i4:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
http.Pusher
}{rw, rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
func (w *rw) Push(target string, opts *http.PushOptions) error {
f := w.w.(http.Pusher).Push
if w.h.Push != nil {
f = w.h.Push(f)
}
return f(target, opts)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}
+278
View File
@@ -0,0 +1,278 @@
// +build !go1.8
// Code generated by "httpsnoop/codegen"; DO NOT EDIT.
package httpsnoop
import (
"bufio"
"io"
"net"
"net/http"
)
// HeaderFunc is part of the http.ResponseWriter interface.
type HeaderFunc func() http.Header
// WriteHeaderFunc is part of the http.ResponseWriter interface.
type WriteHeaderFunc func(code int)
// WriteFunc is part of the http.ResponseWriter interface.
type WriteFunc func(b []byte) (int, error)
// FlushFunc is part of the http.Flusher interface.
type FlushFunc func()
// CloseNotifyFunc is part of the http.CloseNotifier interface.
type CloseNotifyFunc func() <-chan bool
// HijackFunc is part of the http.Hijacker interface.
type HijackFunc func() (net.Conn, *bufio.ReadWriter, error)
// ReadFromFunc is part of the io.ReaderFrom interface.
type ReadFromFunc func(src io.Reader) (int64, error)
// Hooks defines a set of method interceptors for methods included in
// http.ResponseWriter as well as some others. You can think of them as
// middleware for the function calls they target. See Wrap for more details.
type Hooks struct {
Header func(HeaderFunc) HeaderFunc
WriteHeader func(WriteHeaderFunc) WriteHeaderFunc
Write func(WriteFunc) WriteFunc
Flush func(FlushFunc) FlushFunc
CloseNotify func(CloseNotifyFunc) CloseNotifyFunc
Hijack func(HijackFunc) HijackFunc
ReadFrom func(ReadFromFunc) ReadFromFunc
}
// Wrap returns a wrapped version of w that provides the exact same interface
// as w. Specifically if w implements any combination of:
//
// - http.Flusher
// - http.CloseNotifier
// - http.Hijacker
// - io.ReaderFrom
//
// The wrapped version will implement the exact same combination. If no hooks
// are set, the wrapped version also behaves exactly as w. Hooks targeting
// methods not supported by w are ignored. Any other hooks will intercept the
// method they target and may modify the call's arguments and/or return values.
// The CaptureMetrics implementation serves as a working example for how the
// hooks can be used.
func Wrap(w http.ResponseWriter, hooks Hooks) http.ResponseWriter {
rw := &rw{w: w, h: hooks}
_, i0 := w.(http.Flusher)
_, i1 := w.(http.CloseNotifier)
_, i2 := w.(http.Hijacker)
_, i3 := w.(io.ReaderFrom)
switch {
// combination 1/16
case !i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
}{rw, rw}
// combination 2/16
case !i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
io.ReaderFrom
}{rw, rw, rw}
// combination 3/16
case !i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
}{rw, rw, rw}
// combination 4/16
case !i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 5/16
case !i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
}{rw, rw, rw}
// combination 6/16
case !i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 7/16
case !i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw}
// combination 8/16
case !i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 9/16
case i0 && !i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
}{rw, rw, rw}
// combination 10/16
case i0 && !i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
io.ReaderFrom
}{rw, rw, rw, rw}
// combination 11/16
case i0 && !i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
}{rw, rw, rw, rw}
// combination 12/16
case i0 && !i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 13/16
case i0 && i1 && !i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
}{rw, rw, rw, rw}
// combination 14/16
case i0 && i1 && !i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
io.ReaderFrom
}{rw, rw, rw, rw, rw}
// combination 15/16
case i0 && i1 && i2 && !i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
}{rw, rw, rw, rw, rw}
// combination 16/16
case i0 && i1 && i2 && i3:
return struct {
Unwrapper
http.ResponseWriter
http.Flusher
http.CloseNotifier
http.Hijacker
io.ReaderFrom
}{rw, rw, rw, rw, rw, rw}
}
panic("unreachable")
}
type rw struct {
w http.ResponseWriter
h Hooks
}
func (w *rw) Unwrap() http.ResponseWriter {
return w.w
}
func (w *rw) Header() http.Header {
f := w.w.(http.ResponseWriter).Header
if w.h.Header != nil {
f = w.h.Header(f)
}
return f()
}
func (w *rw) WriteHeader(code int) {
f := w.w.(http.ResponseWriter).WriteHeader
if w.h.WriteHeader != nil {
f = w.h.WriteHeader(f)
}
f(code)
}
func (w *rw) Write(b []byte) (int, error) {
f := w.w.(http.ResponseWriter).Write
if w.h.Write != nil {
f = w.h.Write(f)
}
return f(b)
}
func (w *rw) Flush() {
f := w.w.(http.Flusher).Flush
if w.h.Flush != nil {
f = w.h.Flush(f)
}
f()
}
func (w *rw) CloseNotify() <-chan bool {
f := w.w.(http.CloseNotifier).CloseNotify
if w.h.CloseNotify != nil {
f = w.h.CloseNotify(f)
}
return f()
}
func (w *rw) Hijack() (net.Conn, *bufio.ReadWriter, error) {
f := w.w.(http.Hijacker).Hijack
if w.h.Hijack != nil {
f = w.h.Hijack(f)
}
return f()
}
func (w *rw) ReadFrom(src io.Reader) (int64, error) {
f := w.w.(io.ReaderFrom).ReadFrom
if w.h.ReadFrom != nil {
f = w.h.ReadFrom(f)
}
return f(src)
}
type Unwrapper interface {
Unwrap() http.ResponseWriter
}
// Unwrap returns the underlying http.ResponseWriter from within zero or more
// layers of httpsnoop wrappers.
func Unwrap(w http.ResponseWriter) http.ResponseWriter {
if rw, ok := w.(Unwrapper); ok {
// recurse until rw.Unwrap() returns a non-Unwrapper
return Unwrap(rw.Unwrap())
} else {
return w
}
}
+1
View File
@@ -1,4 +1,5 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.
+1
View File
@@ -1,4 +1,5 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.
+11 -36
View File
@@ -426,7 +426,7 @@ func (f Formatter) colon() byte {
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0, 0, nil)
return f.prettyWithFlags(value, 0, 0)
}
const (
@@ -434,13 +434,7 @@ const (
)
// TODO: This is not fast. Most of the overhead goes here.
// value: The value to render
// flags: Bitmask of flags (see above)
// depth: The current depth of nested structs, slices, arrays, and maps
// ptrDepth: The current depth of including pointer dereferences
// ptrMap: A map of pointers already seen, to avoid infinite recursion (usually
// nil unless ptrDepth is large)
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth int, ptrMap map[uintptr]bool) string {
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}
@@ -510,7 +504,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1, ptrDepth+1, ptrMap))
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -582,7 +576,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1, ptrDepth+1, ptrMap))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
continue
}
if name == "" {
@@ -591,7 +585,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
// field names can't contain characters which need escaping
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1, ptrDepth+1, ptrMap))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -618,7 +612,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1, ptrDepth+1, ptrMap))
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
}
buf.WriteByte(']')
return buf.String()
@@ -643,8 +637,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
// key depth is unrelated to overall depth
keystr = f.prettyWithFlags(it.Key().Interface(), 0, 0, ptrDepth, ptrMap)
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
@@ -653,34 +646,16 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth
}
buf.WriteString(keystr)
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1, ptrDepth+1, ptrMap))
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
i++
}
buf.WriteByte('}')
return buf.String()
case reflect.Pointer, reflect.Interface:
case reflect.Ptr, reflect.Interface:
if v.IsNil() {
return "null"
}
// Special case: recursive pointers. For normal use we do not want to
// count pointer dereferences as depth, but if we see the same pointer
// again we have a recursion and need to stop. After a large number of
// pointer dereferences we will start tracking pointers to avoid the
// perf hit of doing it in the normal path.
//
// This should not happen accidentally (e.g. json decoding should never
// do this) but we can handle it gracefully.
if ptrMap != nil && ptrMap[uintptr(v.Pointer())] {
depth = f.opts.MaxLogDepth + 1 // force a depth error
}
const maxDepthFactor = 4 // arbitrary, but we want it large enough to not false-alert
if ptrDepth > f.opts.MaxLogDepth*maxDepthFactor && ptrMap == nil {
ptrMap = map[uintptr]bool{}
}
if ptrMap != nil {
ptrMap[(uintptr)(v.Pointer())] = true
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth, ptrDepth+1, ptrMap)
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
@@ -722,7 +697,7 @@ func isEmpty(v reflect.Value) bool {
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Pointer:
case reflect.Interface, reflect.Ptr:
return v.IsNil()
}
return false
+6 -16
View File
@@ -1,4 +1,5 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
@@ -32,7 +33,7 @@ const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
kvList = attrToKVs(attr, kvList)
return true
})
@@ -48,7 +49,7 @@ func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
kvList = attrToKVs(attr, kvList)
}
l.AddValues(kvList)
return &l
@@ -60,25 +61,14 @@ func (l fnlogger) WithGroup(name string) logr.SlogSink {
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog. maxDepth bounds recursion into nested groups so a
// deeply-nested slog.Group cannot exhaust the stack; it is decremented per group
// level and starts at the Formatter's MaxLogDepth (past which the formatter would
// truncate the rendering anyway).
func attrToKVs(attr slog.Attr, kvList []any, maxDepth int) []any {
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
if maxDepth <= 0 {
// Nesting is too deep to build without risking a stack overflow.
// Stop here; the formatter truncates below MaxLogDepth regardless.
if attr.Key != "" {
kvList = append(kvList, attr.Key, "<max-log-depth-exceeded>")
}
return kvList
}
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs, maxDepth-1)
grpKVs = attrToKVs(attr, grpKVs)
}
if attr.Key == "" {
// slog says we have to inline these
+1
View File
@@ -1,4 +1,5 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
+1
View File
@@ -1,4 +1,5 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
+1
View File
@@ -1,4 +1,5 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
+86 -255
View File
@@ -4,8 +4,6 @@ import (
"os"
"sort"
"strings"
"sync"
"sync/atomic"
"unicode/utf8"
"github.com/clipperhouse/uax29/v2/graphemes"
@@ -20,14 +18,6 @@ var (
// StrictEmojiNeutral should be set false if handle broken fonts
StrictEmojiNeutral bool = true
// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ.
//
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{
EastAsianWidth: false,
@@ -36,73 +26,20 @@ var (
)
var (
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
tablesOnce sync.Once
// strictWidthLUT is mostly built lazily on the first width lookup so
// that importing the package costs neither the build time nor the 2 MB
// of resident memory; see issue #104. Only the entries below
// strictWidthLUTLimit are valid: init fills the first 0x300 entries of
// both planes, and the lazy build fills the rest — never rewriting the
// low region, so readers of it cannot race with the build. The limit
// is loaded with acquire semantics, which makes the non-atomic reads
// of the high region safe once it reports 0x110000. Keeping the whole
// check down to one compare-and-branch matters: RuneWidth is only a
// dozen instructions long.
strictWidthLUT [2][0x110000]byte
strictWidthLUTLimit atomic.Int32
strictWidthLUTOnce sync.Once
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
eastAsianWidth0 [0x300]byte
)
func init() {
initStrictWidthLUTLow()
strictWidthLUTLimit.Store(0x300)
handleEnv()
}
// initStrictWidthLUTLow paints the first 0x300 entries of strictWidthLUT
// from the static interval tables. The result must stay identical to
// runeWidthNoLUT for runes below 0x300, which TestStrictWidthLUT verifies.
func initStrictWidthLUTLow() {
for i := 0; i < 0x300; i++ {
r := rune(i)
w := byte(1)
if r < 0x20 || (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint
w = 0
}
strictWidthLUT[0][i] = w
}
ea := strictWidthLUT[1][:0x300]
fillBytes(ea, 1)
paint := func(t table, w byte) {
for _, iv := range t {
if iv.first >= 0x300 {
break
}
last := iv.last
if last > 0x2FF {
last = 0x2FF
}
fillBytes(ea[iv.first:last+1], w)
}
}
paint(ambiguous, 2)
paint(doublewidth, 2)
// zero-width wins over wide on overlap, so paint it last.
paint(combining, 0)
paint(nonprint, 0)
}
// initTables builds the merged lookup tables. It runs lazily through
// tablesOnce so that merely importing the package stays cheap; see issue
// #104.
func initTables() {
zerowidth = mergeIntervals(combining, nonprint)
widewidth = mergeIntervals(ambiguous, doublewidth)
eastAsianWidth = makeWidthTable(zerowidth, widewidth)
for r := range eastAsianWidth0 {
eastAsianWidth0[r] = byte(runeWidthEastAsian(rune(r)))
}
handleEnv()
}
func mergeIntervals(t1, t2 table) table {
@@ -252,9 +189,50 @@ func inWidthTable(r rune, t widthTable) (int, bool) {
return 0, false
}
func runeWidthNoLUT(r rune, eastAsian, strictEmojiNeutral bool) int {
tablesOnce.Do(initTables)
if !eastAsian {
func runeWidthEastAsian(r rune) int {
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
return 1
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
EastAsianWidth bool
StrictEmojiNeutral bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
// optimized version, verified by TestRuneWidthChecksums()
if !c.EastAsianWidth {
if r < 0x20 {
return 0
}
@@ -275,145 +253,17 @@ func runeWidthNoLUT(r rune, eastAsian, strictEmojiNeutral bool) int {
}
if r < 0x300 {
return int(strictWidthLUT[1][r])
return int(eastAsianWidth0[r])
}
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
if !strictEmojiNeutral && inTable(r, emoji) {
if !c.StrictEmojiNeutral && inTable(r, emoji) {
return 2
}
return 1
}
// fillBytes sets every byte of b to v. It doubles the copied region on each
// iteration so large slices are filled at memcpy speed instead of one byte
// per loop iteration.
func fillBytes(b []byte, v byte) {
if len(b) == 0 {
return
}
b[0] = v
for i := 1; i < len(b); i *= 2 {
copy(b[i:], b[:i])
}
}
// buildStrictWidthLUT builds the strict-width lookup table above 0x300
// exactly once. It paints whole intervals instead of computing every rune
// through the binary searches in runeWidthNoLUT. It must not write below
// 0x300: that region was filled by init and may be read concurrently. The
// result must stay identical to runeWidthNoLUT(r, eastAsian, true), which
// TestStrictWidthLUT verifies.
func buildStrictWidthLUT() {
strictWidthLUTOnce.Do(func() {
tablesOnce.Do(initTables)
// paintHigh fills lut with w over each interval, clipped to 0x300+.
paintHigh := func(lut []byte, first, last rune, w byte) {
if first < 0x300 {
if last < 0x300 {
return
}
first = 0x300
}
fillBytes(lut[first:last+1], w)
}
// EastAsianWidth=false, StrictEmojiNeutral=true
lut := strictWidthLUT[0][:]
fillBytes(lut[0x300:], 1)
for _, iv := range doublewidth {
paintHigh(lut, iv.first, iv.last, 2)
}
// zerowidth is checked before doublewidth, so it wins on overlap.
for _, iv := range zerowidth {
paintHigh(lut, iv.first, iv.last, 0)
}
// EastAsianWidth=true, StrictEmojiNeutral=true
lut = strictWidthLUT[1][:]
fillBytes(lut[0x300:], 1)
for _, iv := range eastAsianWidth {
paintHigh(lut, iv.first, iv.last, iv.width)
}
strictWidthLUTLimit.Store(0x110000)
})
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
EastAsianWidth bool
StrictEmojiNeutral bool
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
ZeroWidthJoiner: ZeroWidthJoiner,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
// This one compare doubles as the range check and the lazy-LUT check:
// out-of-range runes and runes above the built portion of
// strictWidthLUT both take the slow path. Once the LUT is fully built
// the limit is 0x110000 and only invalid runes go slow.
if uint32(r) >= uint32(strictWidthLUTLimit.Load()) {
return c.runeWidthSlow(r)
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
func (c *Condition) runeWidthSlow(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
buildStrictWidthLUT()
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
// This should not be called concurrently with other operations on c.
// If options in c is changed, CreateLUT should be called again.
@@ -435,21 +285,6 @@ func (c *Condition) CreateLUT() {
c.combinedLut = lut
}
// graphemeWidth returns the width of a single grapheme cluster: the sum of
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
// from being counted wider than the two cells terminals give them.
func (c *Condition) graphemeWidth(cluster string) int {
width := 0
for _, r := range cluster {
width += c.RuneWidth(r)
}
if width > 2 {
width = 2
}
return width
}
// StringWidth return width as you can see
func (c *Condition) StringWidth(s string) (width int) {
if len(s) == 1 {
@@ -481,7 +316,14 @@ graphemes:
width = 0
g := graphemes.FromString(s)
for g.Next() {
width += c.graphemeWidth(g.Value())
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
}
}
width += chWidth
}
return
}
@@ -496,7 +338,13 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
chWidth := c.graphemeWidth(g.Value())
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
if width+chWidth > w {
pos = g.Start()
break
@@ -517,7 +365,13 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
g := graphemes.FromString(s)
for g.Next() {
chWidth := c.graphemeWidth(g.Value())
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
if width+chWidth > w {
if width < w {
@@ -536,32 +390,6 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
return prefix + s[pos:]
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
if c.StringWidth(prefix) >= w {
return prefix
}
sw := c.StringWidth(s)
if sw <= w {
return s
}
w -= c.StringWidth(prefix)
var width int
var pos int
g := graphemes.FromString(s)
for g.Next() {
chWidth := c.graphemeWidth(g.Value())
if sw-(width+chWidth) <= w {
pos = g.End()
break
}
width += chWidth
}
return prefix + s[pos:]
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
width := 0
@@ -591,7 +419,11 @@ func (c *Condition) FillLeft(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
return strings.Repeat(" ", count) + s
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return string(b) + s
}
return s
}
@@ -601,7 +433,11 @@ func (c *Condition) FillRight(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
return s + strings.Repeat(" ", count)
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return s + string(b)
}
return s
}
@@ -642,11 +478,6 @@ func TruncateLeft(s string, w int, prefix string) string {
return DefaultCondition.TruncateLeft(s, w, prefix)
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func TruncatePrefix(s string, w int, prefix string) string {
return DefaultCondition.TruncatePrefix(s, w, prefix)
}
// Wrap return string wrapped with w cells
func Wrap(s string, w int) string {
return DefaultCondition.Wrap(s, w)
+25 -37
View File
@@ -5,50 +5,35 @@ package runewidth
import (
"os"
"regexp"
"strings"
)
func mblen(charset string) int {
switch charset {
case "utf-8", "utf8":
return 6
case "jis":
return 8
case "eucjp":
return 3
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
return 2
}
return 1
}
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
// localeCharset extracts the charset part of a locale name of the form
// "ll.CHARSET" or "ll_CC.CHARSET" (two- or three-letter language code,
// optional uppercase country code). It returns "" if locale does not have
// that shape.
func localeCharset(locale string) string {
n := 0
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
n++
}
if n < 2 || n > 3 {
return ""
}
rest := locale[n:]
if len(rest) >= 3 && rest[0] == '_' &&
rest[1] >= 'A' && rest[1] <= 'Z' && rest[2] >= 'A' && rest[2] <= 'Z' {
rest = rest[3:]
}
if len(rest) >= 2 && rest[0] == '.' {
return rest[1:]
}
return ""
var mblenTable = map[string]int{
"utf-8": 6,
"utf8": 6,
"jis": 8,
"eucjp": 3,
"euckr": 2,
"euccn": 2,
"sjis": 2,
"cp932": 2,
"cp51932": 2,
"cp936": 2,
"cp949": 2,
"cp950": 2,
"big5": 2,
"gbk": 2,
"gb2312": 2,
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
if cs := localeCharset(locale); cs != "" {
charset = strings.ToLower(cs)
r := reLoc.FindStringSubmatch(locale)
if len(r) == 2 {
charset = strings.ToLower(r[1])
}
if strings.HasSuffix(charset, "@cjk_narrow") {
@@ -61,7 +46,10 @@ func isEastAsian(locale string) bool {
break
}
}
max := mblen(charset)
max := 1
if m, ok := mblenTable[charset]; ok {
max = m
}
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
+17 -121
View File
@@ -3,127 +3,23 @@
package runewidth
var combining = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
{0x05C7, 0x05C7}, {0x0610, 0x061A}, {0x064B, 0x065F},
{0x0670, 0x0670}, {0x06D6, 0x06DC}, {0x06DF, 0x06E4},
{0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x0711, 0x0711},
{0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3},
{0x07FD, 0x07FD}, {0x0816, 0x0819}, {0x081B, 0x0823},
{0x0825, 0x0827}, {0x0829, 0x082D}, {0x0859, 0x085B},
{0x0897, 0x089F}, {0x08CA, 0x08E1}, {0x08E3, 0x0902},
{0x093A, 0x093A}, {0x093C, 0x093C}, {0x0941, 0x0948},
{0x094D, 0x094D}, {0x0951, 0x0957}, {0x0962, 0x0963},
{0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4},
{0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x09FE, 0x09FE},
{0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42},
{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
{0x0A70, 0x0A71}, {0x0A75, 0x0A75}, {0x0A81, 0x0A82},
{0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8},
{0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF},
{0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F},
{0x0B41, 0x0B44}, {0x0B4D, 0x0B4D}, {0x0B55, 0x0B56},
{0x0B62, 0x0B63}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0},
{0x0BCD, 0x0BCD}, {0x0C00, 0x0C00}, {0x0C04, 0x0C04},
{0x0C3C, 0x0C3C}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48},
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63},
{0x0C81, 0x0C81}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF},
{0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3},
{0x0CF3, 0x0CF3}, {0x0D00, 0x0D01}, {0x0D3B, 0x0D3C},
{0x0D41, 0x0D44}, {0x0D4D, 0x0D4D}, {0x0D62, 0x0D63},
{0x0D81, 0x0D81}, {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A},
{0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EBC},
{0x0EC8, 0x0ECE}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
{0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E},
{0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F8D, 0x0F97},
{0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030},
{0x1032, 0x1037}, {0x1039, 0x103A}, {0x103D, 0x103E},
{0x1058, 0x1059}, {0x105E, 0x1060}, {0x1071, 0x1074},
{0x1082, 0x1082}, {0x1085, 0x1086}, {0x108D, 0x108D},
{0x109D, 0x109D}, {0x135D, 0x135F}, {0x1712, 0x1714},
{0x1732, 0x1733}, {0x1752, 0x1753}, {0x1772, 0x1773},
{0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6},
{0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1885, 0x1886}, {0x18A9, 0x18A9},
{0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932},
{0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1A1B, 0x1A1B},
{0x1A56, 0x1A56}, {0x1A58, 0x1A5E}, {0x1A60, 0x1A60},
{0x1A62, 0x1A62}, {0x1A65, 0x1A6C}, {0x1A73, 0x1A7C},
{0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD}, {0x1AE0, 0x1AEB},
{0x1B00, 0x1B03}, {0x1B34, 0x1B34}, {0x1B36, 0x1B3A},
{0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73},
{0x1B80, 0x1B81}, {0x1BA2, 0x1BA5}, {0x1BA8, 0x1BA9},
{0x1BAB, 0x1BAD}, {0x1BE6, 0x1BE6}, {0x1BE8, 0x1BE9},
{0x1BED, 0x1BED}, {0x1BEF, 0x1BF1}, {0x1C2C, 0x1C33},
{0x1C36, 0x1C37}, {0x1CD0, 0x1CD2}, {0x1CD4, 0x1CE0},
{0x1CE2, 0x1CE8}, {0x1CED, 0x1CED}, {0x1CF4, 0x1CF4},
{0x1CF8, 0x1CF9}, {0x1DC0, 0x1DFF}, {0x20D0, 0x20F0},
{0x2CEF, 0x2CF1}, {0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF},
{0x302A, 0x302D}, {0x3099, 0x309A}, {0xA66F, 0xA672},
{0xA674, 0xA67D}, {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1},
{0xA802, 0xA802}, {0xA806, 0xA806}, {0xA80B, 0xA80B},
{0xA825, 0xA826}, {0xA82C, 0xA82C}, {0xA8C4, 0xA8C5},
{0xA8E0, 0xA8F1}, {0xA8FF, 0xA8FF}, {0xA926, 0xA92D},
{0xA947, 0xA951}, {0xA980, 0xA982}, {0xA9B3, 0xA9B3},
{0xA9B6, 0xA9B9}, {0xA9BC, 0xA9BD}, {0xA9E5, 0xA9E5},
{0xAA29, 0xAA2E}, {0xAA31, 0xAA32}, {0xAA35, 0xAA36},
{0xAA43, 0xAA43}, {0xAA4C, 0xAA4C}, {0xAA7C, 0xAA7C},
{0xAAB0, 0xAAB0}, {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8},
{0xAABE, 0xAABF}, {0xAAC1, 0xAAC1}, {0xAAEC, 0xAAED},
{0xAAF6, 0xAAF6}, {0xABE5, 0xABE5}, {0xABE8, 0xABE8},
{0xABED, 0xABED}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F},
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0},
{0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F},
{0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10D69, 0x10D6D},
{0x10EAB, 0x10EAC}, {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11001, 0x11001}, {0x11038, 0x11046},
{0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11081},
{0x110B3, 0x110B6}, {0x110B9, 0x110BA}, {0x110C2, 0x110C2},
{0x11100, 0x11102}, {0x11127, 0x1112B}, {0x1112D, 0x11134},
{0x11173, 0x11173}, {0x11180, 0x11181}, {0x111B6, 0x111BE},
{0x111C9, 0x111CC}, {0x111CF, 0x111CF}, {0x1122F, 0x11231},
{0x11234, 0x11234}, {0x11236, 0x11237}, {0x1123E, 0x1123E},
{0x11241, 0x11241}, {0x112DF, 0x112DF}, {0x112E3, 0x112EA},
{0x11300, 0x11301}, {0x1133B, 0x1133C}, {0x11340, 0x11340},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x113BB, 0x113C0},
{0x113CE, 0x113CE}, {0x113D0, 0x113D0}, {0x113D2, 0x113D2},
{0x113E1, 0x113E2}, {0x11438, 0x1143F}, {0x11442, 0x11444},
{0x11446, 0x11446}, {0x1145E, 0x1145E}, {0x114B3, 0x114B8},
{0x114BA, 0x114BA}, {0x114BF, 0x114C0}, {0x114C2, 0x114C3},
{0x115B2, 0x115B5}, {0x115BC, 0x115BD}, {0x115BF, 0x115C0},
{0x115DC, 0x115DD}, {0x11633, 0x1163A}, {0x1163D, 0x1163D},
{0x1163F, 0x11640}, {0x116AB, 0x116AB}, {0x116AD, 0x116AD},
{0x116B0, 0x116B5}, {0x116B7, 0x116B7}, {0x1171D, 0x1171D},
{0x1171F, 0x1171F}, {0x11722, 0x11725}, {0x11727, 0x1172B},
{0x1182F, 0x11837}, {0x11839, 0x1183A}, {0x1193B, 0x1193C},
{0x1193E, 0x1193E}, {0x11943, 0x11943}, {0x119D4, 0x119D7},
{0x119DA, 0x119DB}, {0x119E0, 0x119E0}, {0x11A01, 0x11A0A},
{0x11A33, 0x11A38}, {0x11A3B, 0x11A3E}, {0x11A47, 0x11A47},
{0x11A51, 0x11A56}, {0x11A59, 0x11A5B}, {0x11A8A, 0x11A96},
{0x11A98, 0x11A99}, {0x11B60, 0x11B60}, {0x11B62, 0x11B64},
{0x11B66, 0x11B66}, {0x11C30, 0x11C36}, {0x11C38, 0x11C3D},
{0x11C3F, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CAA, 0x11CB0},
{0x11CB2, 0x11CB3}, {0x11CB5, 0x11CB6}, {0x11D31, 0x11D36},
{0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D45},
{0x11D47, 0x11D47}, {0x11D90, 0x11D91}, {0x11D95, 0x11D95},
{0x11D97, 0x11D97}, {0x11EF3, 0x11EF4}, {0x11F00, 0x11F01},
{0x11F36, 0x11F3A}, {0x11F40, 0x11F40}, {0x11F42, 0x11F42},
{0x11F5A, 0x11F5A}, {0x13440, 0x13440}, {0x13447, 0x13455},
{0x1611E, 0x16129}, {0x1612D, 0x1612F}, {0x16AF0, 0x16AF4},
{0x16B30, 0x16B36}, {0x16F4F, 0x16F4F}, {0x16F8F, 0x16F92},
{0x16FE4, 0x16FE4}, {0x1BC9D, 0x1BC9E}, {0x1CF00, 0x1CF2D},
{0x1CF30, 0x1CF46}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
{0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
{0x1D242, 0x1D244}, {0x1DA00, 0x1DA36}, {0x1DA3B, 0x1DA6C},
{0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84}, {0x1DA9B, 0x1DA9F},
{0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
{0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
{0x1E08F, 0x1E08F}, {0x1E130, 0x1E136}, {0x1E2AE, 0x1E2AE},
{0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF}, {0x1E5EE, 0x1E5EF},
{0x1E6E3, 0x1E6E3}, {0x1E6E6, 0x1E6E6}, {0x1E6EE, 0x1E6EF},
{0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A},
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0CF3, 0x0CF3},
{0x0D00, 0x0D01}, {0x135D, 0x135F}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD},
{0x1AE0, 0x1AEB}, {0x1B6B, 0x1B73}, {0x1DC0, 0x1DFF},
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
{0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, {0x101FD, 0x101FD},
{0x10376, 0x1037A}, {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11300, 0x11301}, {0x1133B, 0x1133C},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x16AF0, 0x16AF4},
{0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169},
{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E08F, 0x1E08F}, {0x1E8D0, 0x1E8D6},
{0xE0100, 0xE01EF},
}
-12
View File
@@ -9,7 +9,6 @@ issues:
linters:
enable:
- errorlint
- gosec
- unconvert
- unparam
exclusions:
@@ -17,18 +16,7 @@ linters:
presets:
- comments
- std-error-handling
rules:
# Ignore "G204: Subprocess launched with a potential tainted input or cmd arguments"
- path: '(.+)_test\.go'
linters:
- gosec
text: 'G204: Subprocess launched'
settings:
gosec:
excludes:
- G301 # Expect directory permissions to be 0750 or less
- G304 # Potential file inclusion via variable
- G306 # Expect WriteFile permissions to be 0600 or less
staticcheck:
# Enable all options, with some exceptions.
# For defaults, see https://golangci-lint.run/usage/linters/#staticcheck
+150 -409
View File
@@ -8,16 +8,13 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/containerd/log"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/patternmatcher"
"github.com/moby/sys/sequential"
"github.com/moby/sys/user"
@@ -49,18 +46,9 @@ type (
// TarOptions wraps the tar options.
TarOptions struct {
// IncludeFiles lists archive-relative paths to include.
// Paths use POSIX ('/') separators.
IncludeFiles []string
// ExcludePatterns lists archive-relative exclude patterns.
// Patterns use POSIX ('/') separators, matching patternmatcher semantics.
ExcludePatterns []string
Compression compression.Compression
// NoLchown disables applying ownership from the archive to extracted files
// and directories. Despite its historical name, it applies to all ownership
// changes, leaving extracted filesystem objects owned by the user performing
// the extraction.
IncludeFiles []string
ExcludePatterns []string
Compression compression.Compression
NoLchown bool
IDMap user.IdentityMapping
ChownOpts *ChownOpts
@@ -82,22 +70,9 @@ type (
// were probably in the archive for a reason, so set this option at
// your own peril.
BestEffortXattrs bool
// internalOptions contains options for use by packages within this module.
internalOptions *archiveoptions.Options
}
)
// WithProcSelfFD returns a copy of opts prepared for extraction in a
// filesystem context where /proc/self/fd may not be accessible by path.
//
// The caller must invoke the returned cleanup function after extraction
// completes. On platforms that do not use /proc/self/fd for extraction,
// the returned cleanup function is a no-op.
func WithProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
return withProcSelfFD(opts)
}
// Archiver implements the Archiver interface and allows the reuse of most utility functions of
// this package with a pluggable Untar function. Also, to facilitate the passing of specific id
// mappings for untar, an Archiver can be created with maps which will then be passed to Untar operations.
@@ -111,33 +86,10 @@ func NewDefaultArchiver() *Archiver {
return &Archiver{Untar: Untar}
}
// isPathEscapes reports whether err is os.Root's path-containment error.
//
// os.Root currently returns an unexported errPathEscapes sentinel, so callers
// cannot detect it with errors.Is. Keep the string comparison isolated here
// until Go exports the error; see https://go.dev/issue/74640.
func isPathEscapes(err error) bool {
// https://github.com/golang/go/blob/go1.26.5/src/os/file.go#L421
const errPathEscapes = "path escapes from parent"
for err != nil {
if errors.Unwrap(err) == nil {
return err.Error() == errPathEscapes
}
err = errors.Unwrap(err)
}
return false
}
// breakoutErr marks errors caused by archive breakout attempts.
// Unit tests use it to distinguish expected breakout failures from other
// errors.
type breakoutErr struct{ error }
func breakoutError(err error) error {
return &breakoutErr{error: err}
}
func (e *breakoutErr) Unwrap() error { return e.error }
// breakoutError is used to differentiate errors related to breaking out
// When testing archive breakout in the unit tests, this error is expected
// in order for the test to pass.
type breakoutError error
const (
AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts
@@ -146,17 +98,17 @@ const (
// IsArchivePath checks if the (possibly compressed) file at the given path
// starts with a tar file header.
func IsArchivePath(filePath string) bool {
file, err := os.Open(filePath)
func IsArchivePath(path string) bool {
file, err := os.Open(path)
if err != nil {
return false
}
defer func() { _ = file.Close() }()
defer file.Close()
rdr, err := compression.DecompressStream(file)
if err != nil {
return false
}
defer func() { _ = rdr.Close() }()
defer rdr.Close()
r := tar.NewReader(rdr)
_, err = r.Next()
return err == nil
@@ -177,10 +129,8 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
go func() {
tarReader := tar.NewReader(inputTarStream)
tarWriter := tar.NewWriter(pipeWriter)
defer func() {
_ = tarWriter.Close()
_ = inputTarStream.Close()
}()
defer inputTarStream.Close()
defer tarWriter.Close()
modify := func(name string, original *tar.Header, modifier TarModifierFunc, tarReader io.Reader) error {
header, data, err := modifier(name, original, tarReader)
@@ -214,7 +164,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
break
}
if err != nil {
_ = pipeWriter.CloseWithError(err)
pipeWriter.CloseWithError(err)
return
}
@@ -222,11 +172,11 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
if !ok {
// No modifiers for this file, copy the header and data
if err := tarWriter.WriteHeader(originalHeader); err != nil {
_ = pipeWriter.CloseWithError(err)
pipeWriter.CloseWithError(err)
return
}
if err := copyWithBuffer(tarWriter, tarReader); err != nil {
_ = pipeWriter.CloseWithError(err)
pipeWriter.CloseWithError(err)
return
}
continue
@@ -234,7 +184,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
delete(mods, originalHeader.Name)
if err := modify(originalHeader.Name, originalHeader, modifier, tarReader); err != nil {
_ = pipeWriter.CloseWithError(err)
pipeWriter.CloseWithError(err)
return
}
}
@@ -242,12 +192,12 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
// Apply the modifiers that haven't matched any files in the archive
for name, modifier := range mods {
if err := modify(name, nil, modifier, nil); err != nil {
_ = pipeWriter.CloseWithError(err)
pipeWriter.CloseWithError(err)
return
}
}
_ = pipeWriter.Close()
pipeWriter.Close()
}()
return pipeReader
}
@@ -268,7 +218,7 @@ func FileInfoHeader(name string, fi os.FileInfo, link string) (*tar.Header, erro
hdr.ModTime = hdr.ModTime.Truncate(time.Second)
hdr.AccessTime = time.Time{}
hdr.ChangeTime = time.Time{}
hdr.Mode = chmodTarEntry(hdr.Mode)
hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
hdr.Name = canonicalTarName(name, fi.IsDir())
return hdr, nil
}
@@ -277,7 +227,7 @@ const paxSchilyXattr = "SCHILY.xattr."
// ReadSecurityXattrToTarHeader reads security.capability xattr from filesystem
// to a tar header
func ReadSecurityXattrToTarHeader(filePath string, hdr *tar.Header) error {
func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
const (
// Values based on linux/include/uapi/linux/capability.h
xattrCapsSz2 = 20
@@ -285,7 +235,7 @@ func ReadSecurityXattrToTarHeader(filePath string, hdr *tar.Header) error {
vfsCapRevision2 = 2
vfsCapRevision3 = 3
)
capability, _ := lgetxattr(filePath, "security.capability")
capability, _ := lgetxattr(path, "security.capability")
if capability != nil {
if capability[versionOffset] == vfsCapRevision3 {
// Convert VFS_CAP_REVISION_3 to VFS_CAP_REVISION_2 as root UID makes no
@@ -303,7 +253,7 @@ func ReadSecurityXattrToTarHeader(filePath string, hdr *tar.Header) error {
type tarWhiteoutConverter interface {
ConvertWrite(*tar.Header, string, os.FileInfo) (*tar.Header, error)
ConvertRead(*os.Root, *tar.Header, string) (bool, error)
ConvertRead(*tar.Header, string) (bool, error)
}
type tarAppender struct {
@@ -342,10 +292,9 @@ func canonicalTarName(name string, isDir bool) string {
return name
}
// addTarFile adds to the tar archive a file from `srcPath` as `name`
func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
archivePath = filepath.ToSlash(archivePath)
fi, err := os.Lstat(srcPath)
// addTarFile adds to the tar archive a file from `path` as `name`
func (ta *tarAppender) addTarFile(path, name string) error {
fi, err := os.Lstat(path)
if err != nil {
return err
}
@@ -353,17 +302,17 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
var link string
if fi.Mode()&os.ModeSymlink != 0 {
var err error
link, err = os.Readlink(srcPath)
link, err = os.Readlink(path)
if err != nil {
return err
}
}
hdr, err := FileInfoHeader(archivePath, fi, link)
hdr, err := FileInfoHeader(name, fi, link)
if err != nil {
return err
}
if err := ReadSecurityXattrToTarHeader(srcPath, hdr); err != nil {
if err := ReadSecurityXattrToTarHeader(path, hdr); err != nil {
return err
}
@@ -372,7 +321,7 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
if !fi.IsDir() && hasHardlinks(fi) {
inode, err := getInodeFromStat(fi.Sys())
if err != nil {
return fmt.Errorf("unexpected file info for %q: %w", srcPath, err)
return err
}
// a link should have a name that it links too
// and that linked name should be first in the tar archive
@@ -381,7 +330,7 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
hdr.Linkname = oldpath
hdr.Size = 0 // This Must be here for the writer math to add up!
} else {
ta.SeenFiles[inode] = hdr.Name
ta.SeenFiles[inode] = name
}
}
@@ -392,7 +341,7 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
// handle re-mapping container ID mappings back to host ID mappings before
// writing tar headers/files. We skip whiteout files because they were written
// by the kernel and already have proper ownership relative to the host
if !isOverlayWhiteout && !strings.HasPrefix(path.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() {
if !isOverlayWhiteout && !strings.HasPrefix(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() {
uid, gid, err := getFileUIDGID(fi.Sys())
if err != nil {
return err
@@ -410,7 +359,7 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
}
if ta.WhiteoutConverter != nil {
wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, srcPath, fi)
wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, path, fi)
if err != nil {
return err
}
@@ -421,12 +370,12 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
// hdr may have been updated to be a whiteout with returning
// a whiteout header
if wo != nil {
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
return fmt.Errorf("tar: cannot use whiteout for non-empty file %q", hdr.Name)
}
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
return err
}
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
return fmt.Errorf("tar: cannot use whiteout for non-empty file")
}
hdr = wo
}
}
@@ -438,13 +387,13 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
if hdr.Typeflag == tar.TypeReg && hdr.Size > 0 {
// We use sequential file access to avoid depleting the standby list on
// Windows. On Linux, this equates to a regular os.Open.
file, err := sequential.Open(srcPath)
file, err := sequential.Open(path)
if err != nil {
return err
}
err = copyWithBuffer(ta.TarWriter, file)
_ = file.Close()
file.Close()
if err != nil {
return err
}
@@ -453,99 +402,11 @@ func (ta *tarAppender) addTarFile(srcPath, archivePath string) error {
return nil
}
// resolveArchivePath resolves intermediate symlinks in name using chroot-like
// semantics when os.Root cannot traverse them. The final path component is
// intentionally preserved because archive extraction may create or replace it.
//
// This is a compatibility workaround rather than the preferred long-term
// implementation. It resolves the path separately before the actual operation,
// so a concurrent filesystem change may cause the operation to affect a
// different path within root. The subsequent os.Root operation still confines
// the operation to root and prevents such a change from escaping it.
//
// Paths with missing components are supported. Existing symlinks are resolved,
// and any remaining nonexistent components are retained for later creation.
//
// This helper should eventually be replaced by handle-relative resolution and
// operations with resolve-in-root semantics, avoiding the resolution/use race
// and repeated path traversal.
func resolveArchivePath(root *os.Root, name string) (string, error) {
parent, base := filepath.Split(name)
if parent == "" {
return name, nil
}
parent = filepath.Clean(parent)
// Follow the final parent component: it is an intermediate component of name,
// and an absolute symlink there must trigger the resolve-in-root fallback.
_, statErr := root.Stat(parent)
switch {
case statErr == nil:
return name, nil
case !os.IsNotExist(statErr) && !isPathEscapes(statErr):
return "", statErr
}
// Resolve the parent both to handle ENOENT from missing components or dangling
// symlinks, and to determine whether an os.Root breakout was caused by an
// absolute symlink. Relative symlink escapes preserve the original Stat error.
resolved, err := resolveFSRootPath(root.Name(), parent)
if err != nil {
return "", err
}
if isPathEscapes(statErr) && (!resolved.followedAbsoluteLink || resolved.relativeEscapeBeforeAbsolute) {
return "", statErr
}
relParent, err := filepath.Rel(root.Name(), resolved.path)
if err != nil {
return "", breakoutError(fmt.Errorf(
"could not make resolved parent %q relative to root %q: %w",
resolved.path,
root.Name(),
err,
))
}
if relParent != "." && !filepath.IsLocal(relParent) {
return "", breakoutError(fmt.Errorf(
"resolved parent %q escapes root %q",
resolved.path,
root.Name(),
))
}
return filepath.Join(relParent, base), nil
}
// resolveHardlinkTarget validates a POSIX hardlink target and resolves it to
// the native, root-relative filesystem path used for extraction.
func resolveHardlinkTarget(root *os.Root, linkname string) (string, error) {
cleaned := path.Clean(linkname)
if strings.HasPrefix(cleaned, "/") {
// Some image builders (e.g. kaniko) write hardlink targets as absolute
// paths. Resolve those relative to the extraction root, with chroot-like
// semantics matching absolute symlink targets. Strip the root from the
// original linkname rather than the cleaned one so that ".." components
// are not collapsed against "/" but instead rejected below.
cleaned = path.Clean(strings.TrimLeft(linkname, "/"))
}
if cleaned == "." || !filepath.IsLocal(cleaned) {
return "", breakoutError(fmt.Errorf("invalid hardlink target %q", linkname))
}
return resolveArchivePath(root, filepath.FromSlash(cleaned))
}
// createTarFile extracts a single tar entry into the given root. dstPath is the
// root-relative path of the entry being extracted, in native (host-separator)
// form so it can be passed directly to os.Root methods and fsRootPath.
func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error {
func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error {
var (
Lchown = true
inUserns, bestEffortXattrs bool
chownOpts *ChownOpts
internalOpts *archiveoptions.Options
)
// TODO(thaJeztah): make opts a required argument.
@@ -554,7 +415,6 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
inUserns = opts.InUserNS // TODO(thaJeztah): consider deprecating opts.InUserNS and detect locally.
chownOpts = opts.ChownOpts
bestEffortXattrs = opts.BestEffortXattrs
internalOpts = opts.internalOptions
}
// hdr.Mode is in linux format, which we can use for sycalls,
@@ -562,35 +422,20 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// so use hdrInfo.Mode() (they differ for e.g. setuid bits)
hdrInfo := hdr.FileInfo()
var hardlinkTarget string
if hdr.Typeflag == tar.TypeLink {
var err error
hardlinkTarget, err = resolveHardlinkTarget(root, hdr.Linkname)
if err != nil {
return err
}
}
switch hdr.Typeflag {
case tar.TypeDir:
// Create directory unless it already exists as one; merge in that case.
// os.Root.Mkdir only accepts the nine least-significant permission
// bits; special bits (setuid, setgid, sticky) are applied afterward
// by handleLChmod via root.Chmod.
if fi, err := root.Lstat(dstPath); err != nil || !fi.IsDir() {
if err := root.Mkdir(dstPath, hdrInfo.Mode()&0o777); err != nil {
// Create directory unless it exists as a directory already.
// In that case we just want to merge the two
if fi, err := os.Lstat(path); err != nil || !fi.IsDir() {
if err := os.Mkdir(path, hdrInfo.Mode()); err != nil {
return err
}
}
case tar.TypeReg:
// Source is a regular file. Use os.Root.OpenFile so that all
// path resolution is bounded within root using openat(2) semantics.
// os.Root.OpenFile only accepts the nine least-significant permission
// bits; special bits are applied afterward by handleLChmod.
// We use sequential file access to avoid depleting the standby list
// on Windows (go1.26). On Linux, this equates to a regular os.OpenFile.
file, err := root.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|windows_O_FILE_FLAG_SEQUENTIAL_SCAN, hdrInfo.Mode()&0o777)
// Source is regular file. We use sequential file access to avoid depleting
// the standby list on Windows. On Linux, this equates to a regular os.OpenFile.
file, err := sequential.OpenFile(path, os.O_CREATE|os.O_WRONLY, hdrInfo.Mode())
if err != nil {
return err
}
@@ -602,41 +447,47 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
case tar.TypeBlock, tar.TypeChar:
if inUserns { // cannot create devices in a userns
log.G(context.TODO()).WithFields(log.Fields{"path": dstPath, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
log.G(context.TODO()).WithFields(log.Fields{"path": path, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
return nil
}
if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); err != nil {
// Handle this is an OS-specific way
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
return err
}
case tar.TypeFifo:
if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); err != nil {
// Handle this is an OS-specific way
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
if inUserns && errors.Is(err, syscall.EPERM) {
// In most cases, cannot create a fifo if running in user namespace
log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": dstPath, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": path, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
return nil
}
return err
}
case tar.TypeLink:
if err := root.Link(hardlinkTarget, dstPath); err != nil {
// #nosec G305 -- The target path is checked for path traversal.
targetPath := filepath.Join(extractDir, hdr.Linkname)
// check for hardlink breakout
if !strings.HasPrefix(targetPath, extractDir) {
return breakoutError(fmt.Errorf("invalid hardlink %q -> %q", targetPath, hdr.Linkname))
}
if err := os.Link(targetPath, path); err != nil {
return err
}
case tar.TypeSymlink:
// Symlink targets are archive data, not filesystem paths. Preserve the
// target verbatim rather than cleaning or converting it (filepath.FromSlash).
linkTarget := hdr.Linkname
// path -> hdr.Linkname = targetPath
// e.g. /extractDir/path/to/symlink -> ../2/file = /extractDir/path/2/file
targetPath := filepath.Join(filepath.Dir(path), hdr.Linkname) // #nosec G305 -- The target path is checked for path traversal.
// os.Root.Symlink contains the symlink's location (newname) within
// root but stores the target (oldname) verbatim, so absolute targets
// such as /usr/lib -- common and legitimate in container images -- are
// preserved rather than rejected. The symlink node is therefore always
// created within root via openat(2) semantics, without resolving to an
// absolute path; containment applies when the symlink is followed, not
// at creation.
if err := root.Symlink(linkTarget, dstPath); err != nil {
// the reason we don't need to check symlinks in the path (with FollowSymlinkInScope) is because
// that symlink would first have to be created, which would be caught earlier, at this very check:
if !strings.HasPrefix(targetPath, extractDir) {
return breakoutError(fmt.Errorf("invalid symlink %q -> %q", path, hdr.Linkname))
}
if err := os.Symlink(hdr.Linkname, path); err != nil {
return err
}
@@ -653,31 +504,22 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
if chownOpts == nil {
chownOpts = &ChownOpts{UID: hdr.Uid, GID: hdr.Gid}
}
if err := root.Lchown(dstPath, chownOpts.UID, chownOpts.GID); err != nil {
if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
var msg string
if inUserns && errors.Is(err, syscall.EINVAL) {
msg = " (try increasing the number of subordinate IDs in /etc/subuid and /etc/subgid)"
}
return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", dstPath, hdr.Uid, hdr.Gid, msg, err)
return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", path, hdr.Uid, hdr.Gid, msg, err)
}
}
var xattrErrs []string
absPath := sync.OnceValues(func() (string, error) {
return fsRootPath(root.Name(), dstPath)
})
for key, value := range hdr.PAXRecords {
xattr, ok := strings.CutPrefix(key, paxSchilyXattr)
if !ok {
continue
}
// os.Root has no xattr support; use the absolute path derived from
// the root so the path remains bounded.
ap, err := absPath()
if err != nil {
return err
}
if err := lsetxattr(ap, xattr, []byte(value), 0); err != nil {
if err := lsetxattr(path, xattr, []byte(value), 0); err != nil {
if bestEffortXattrs && errors.Is(err, syscall.ENOTSUP) || errors.Is(err, syscall.EPERM) {
// EPERM occurs if modifying xattrs is not allowed. This can
// happen when running in userns with restrictions (ChromeOS).
@@ -696,43 +538,39 @@ func createTarFile(root *os.Root, dstPath string, hdr *tar.Header, reader io.Rea
// There is no LChmod, so ignore mode for symlink. Also, this
// must happen after chown, as that can modify the file mode
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
if err := handleLChmod(hdr, path, hdrInfo); err != nil {
return err
}
aTime := boundTime(latestTime(hdr.AccessTime, hdr.ModTime))
mTime := boundTime(hdr.ModTime)
switch hdr.Typeflag {
case tar.TypeSymlink:
// Apply timestamps to the symlink itself (AT_SYMLINK_NOFOLLOW).
if err := lchtimes(root, dstPath, aTime, mTime); err != nil {
return err
}
case tar.TypeLink:
// Follow the hardlink only when its target is not itself a symlink.
fi, err := root.Lstat(hardlinkTarget)
if err == nil && fi.Mode()&os.ModeSymlink == 0 {
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
// chtimes doesn't support a NOFOLLOW flag atm
if hdr.Typeflag == tar.TypeLink {
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if err := chtimes(path, aTime, mTime); err != nil {
return err
}
}
default:
// All other file types follow symlinks.
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
} else if hdr.Typeflag != tar.TypeSymlink {
if err := chtimes(path, aTime, mTime); err != nil {
return err
}
} else {
if err := lchtimes(path, aTime, mTime); err != nil {
return err
}
}
return nil
}
// Tar creates an archive from the directory at `srcPath`, and returns it as a
// Tar creates an archive from the directory at `path`, and returns it as a
// stream of bytes.
func Tar(srcPath string, comp compression.Compression) (io.ReadCloser, error) {
return TarWithOptions(srcPath, &TarOptions{Compression: comp})
func Tar(path string, comp compression.Compression) (io.ReadCloser, error) {
return TarWithOptions(path, &TarOptions{Compression: comp})
}
// TarWithOptions creates an archive from the directory at `srcPath`, only including files whose relative
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative
// paths are included in `options.IncludeFiles` (if non-nil) or not in `options.ExcludePatterns`.
func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error) {
tb, err := NewTarballer(srcPath, options)
@@ -801,13 +639,13 @@ func (t *Tarballer) Do() {
defer func() {
// Make sure to check the error on Close.
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
if err := ta.TarWriter.Close(); err != nil {
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
}
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
if err := t.compressWriter.Close(); err != nil {
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
}
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
if err := t.pipeWriter.Close(); err != nil {
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
}
}()
@@ -965,28 +803,11 @@ func (t *Tarballer) Do() {
}
}
// unpackedDir records a directory whose mtime must be restored after all
// entries are extracted, along with the root-relative entry name used during
// extraction.
type unpackedDir struct {
hdr *tar.Header
name string // root-relative entry name
}
// Unpack unpacks the decompressedArchive to dest with options.
func Unpack(decompressedArchive io.Reader, dest string, options *TarOptions) error {
if options == nil {
options = &TarOptions{}
}
root, err := os.OpenRoot(dest)
if err != nil {
return err
}
defer func() { _ = root.Close() }()
tr := tar.NewReader(decompressedArchive)
var dirs []unpackedDir
var dirs []*tar.Header
whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat)
// Iterate through the files in the archive.
@@ -1007,53 +828,48 @@ loop:
continue
}
// Strip a leading "/" so absolute entries stay root-relative, and
// normalize the POSIX tar path. Skip entries referring to the extraction
// root and reject paths that escape it.
name := path.Clean(strings.TrimLeft(hdr.Name, "/"))
if name == "." {
continue
}
if !filepath.IsLocal(name) {
return breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name))
}
// Normalize name, for safety and for a simple is-root check
// This keeps "../" as-is, but normalizes "/../" to "/". Or Windows:
// This keeps "..\" as-is, but normalizes "\..\" to "\".
hdr.Name = filepath.Clean(hdr.Name)
for _, exclude := range options.ExcludePatterns {
if strings.HasPrefix(name, exclude) {
if strings.HasPrefix(hdr.Name, exclude) {
continue loop
}
}
hdr.Name = name
// Skip entries whose name (or hardlink target) Windows cannot represent.
if err := unrepresentableOnWindows(hdr); err != nil {
log.G(context.TODO()).Warnf("Windows: ignoring entry: %v", err)
continue loop
}
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// hdr.Name stays POSIX (forward-slash) for logical string checks.
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
// Ensure that the parent directory exists.
err = createImpliedDirectories(dest, hdr, options)
if err != nil {
return err
}
// If dstPath exists we almost always just want to remove and replace it.
// #nosec G305 -- The joined path is checked for path traversal.
path := filepath.Join(dest, hdr.Name)
rel, err := filepath.Rel(dest, path)
if err != nil {
return err
}
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
}
// If path exits we almost always just want to remove and replace it
// The only exception is when it is a directory *and* the file from
// the layer is also a directory. Then we want to merge them (i.e.
// just apply the metadata from the layer).
if fi, err := root.Lstat(dstPath); err == nil {
if fi, err := os.Lstat(path); err == nil {
if options.NoOverwriteDirNonDir && fi.IsDir() && hdr.Typeflag != tar.TypeDir {
// If NoOverwriteDirNonDir is true then we cannot replace
// an existing directory with a non-directory from the archive.
return fmt.Errorf("cannot overwrite directory %q with non-directory %q", hdr.Name, dest)
return fmt.Errorf("cannot overwrite directory %q with non-directory %q", path, dest)
}
if options.NoOverwriteDirNonDir && !fi.IsDir() && hdr.Typeflag == tar.TypeDir {
// If NoOverwriteDirNonDir is true then we cannot replace
// an existing non-directory with a directory from the archive.
return fmt.Errorf("cannot overwrite non-directory %q with directory %q", hdr.Name, dest)
return fmt.Errorf("cannot overwrite non-directory %q with directory %q", path, dest)
}
if fi.IsDir() && hdr.Name == "." {
@@ -1061,7 +877,7 @@ loop:
}
if !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
if err := root.RemoveAll(dstPath); err != nil {
if err := os.RemoveAll(path); err != nil {
return err
}
}
@@ -1071,16 +887,8 @@ loop:
return err
}
// Ensure that the parent directory exists.
//
// This must be done before whiteoutConverter.ConvertRead, which
// may set xattrs on the directory or create whiteout files.
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return err
}
if whiteoutConverter != nil {
writeFile, err := whiteoutConverter.ConvertRead(root, hdr, dstPath)
writeFile, err := whiteoutConverter.ConvertRead(hdr, path)
if err != nil {
return err
}
@@ -1089,121 +897,51 @@ loop:
}
}
if err := createTarFile(root, dstPath, hdr, tr, options); err != nil {
if err := createTarFile(path, dest, hdr, tr, options); err != nil {
return err
}
// Directory mtimes must be handled at the end to avoid further
// file creation in them to modify the directory mtime
if hdr.Typeflag == tar.TypeDir {
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
dirs = append(dirs, hdr)
}
}
for _, d := range dirs {
aTime := boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime))
if err := chtimes(root, d.name, aTime, boundTime(d.hdr.ModTime)); err != nil {
for _, hdr := range dirs {
// #nosec G305 -- The header was checked for path traversal before it was appended to the dirs slice.
path := filepath.Join(dest, hdr.Name)
if err := chtimes(path, boundTime(latestTime(hdr.AccessTime, hdr.ModTime)), boundTime(hdr.ModTime)); err != nil {
return err
}
}
return nil
}
// unrepresentableOnWindows returns an error describing why a tar entry cannot
// be faithfully created on Windows, or nil if it can (always on non-Windows).
// On Windows ":" is illegal in a filename and "\" is a path separator, so a tar
// name or hardlink target containing them (they use POSIX semantics) would be
// misinterpreted by os.Root (e.g. "a\b" resolved as two components). Symlink
// targets are stored verbatim (not resolved at creation), so they are exempt.
func unrepresentableOnWindows(hdr *tar.Header) error {
if runtime.GOOS != "windows" {
return nil
}
if strings.ContainsAny(hdr.Name, `:\`) {
return fmt.Errorf("entry name %q contains a character Windows cannot represent in a path", hdr.Name)
}
// A hardlink target is resolved within the root by os.Root.Link; a symlink
// target is stored verbatim, so only hardlinks need the target checked.
if hdr.Typeflag == tar.TypeLink && strings.ContainsAny(hdr.Linkname, `:\`) {
return fmt.Errorf("hardlink target %q contains a character Windows cannot represent in a path", hdr.Linkname)
}
return nil
}
// createImpliedDirectories creates all parent directories of dstPath with
// default permissions if they do not already exist. This is necessary because
// the tar format permits implicit directories whose existence is defined only
// by file paths, without corresponding directory headers from which metadata
// could be restored.
// createImpliedDirectories will create all parent directories of the current path with default permissions, if they do
// not already exist. This is possible as the tar format supports 'implicit' directories, where their existence is
// defined by the paths of files in the tar, but there are no header entries for the directories themselves, and thus
// we most both create them and choose metadata like permissions.
//
// The caller must pass a normalized, root-relative local path. Any archive-path
// conversion and resolve-in-root handling must already have been applied.
// Directory creation is performed through root, so it remains confined to the
// extraction destination even if the destination tree changes concurrently.
func createImpliedDirectories(root *os.Root, dstPath string, options *TarOptions) error {
parent := filepath.Dir(dstPath)
// The caller should have performed filepath.Clean(hdr.Name), so hdr.Name will now be in the filepath format for the OS
// on which the daemon is running. This precondition is required because this function assumes a OS-specific path
// separator when checking that a path is not the root.
func createImpliedDirectories(dest string, hdr *tar.Header, options *TarOptions) error {
// Not the root directory, ensure that the parent directory exists
if !strings.HasSuffix(hdr.Name, string(os.PathSeparator)) {
parent := filepath.Dir(hdr.Name)
parentPath := filepath.Join(dest, parent)
if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) {
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
// Skip when the parent is the root itself; nothing to create.
if parent == "." || parent == "" {
return nil
}
if _, err := root.Lstat(parent); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
// RootPair() is confined inside this loop as most cases will not require a call, so we can spend some
// unneeded function calls in the uncommon case to encapsulate logic -- implied directories are a niche
// usage that reduces the portability of an image.
uid, gid := options.IDMap.RootPair()
// Similar to [user.MkdirAllAndChown]
//
// [user.MkdirAllAndChown]: https://pkg.go.dev/github.com/moby/sys/user#MkdirAllAndChown
var cur string
for c := range strings.SplitSeq(parent, string(os.PathSeparator)) {
if c == "" {
continue
}
cur = filepath.Join(cur, c)
if err := root.Mkdir(cur, ImpliedDirectoryMode); err != nil {
if !errors.Is(err, os.ErrExist) {
return err
}
fi, err := root.Stat(cur)
err = user.MkdirAllAndChown(parentPath, ImpliedDirectoryMode, uid, gid, user.WithOnlyNew)
if err != nil {
return err
}
if fi.IsDir() {
continue
}
return &os.PathError{Op: "mkdir", Path: cur, Err: syscall.ENOTDIR}
}
if options.NoLchown {
continue
}
// Only the successful Mkdir case is newly-created.
dir, err := root.Open(cur)
if err != nil {
return err
}
if uid != 0 || gid != 0 {
if err := dir.Chown(uid, gid); err != nil {
_ = dir.Close()
return err
}
}
// root.Mkdir applies the mode subject to the process umask, so
// re-apply it with Chmod to guarantee ImpliedDirectoryMode
// independent of umask, matching the previous MkdirAllAndChown
// behavior.
if err := dir.Chmod(ImpliedDirectoryMode); err != nil {
_ = dir.Close()
return err
}
if err := dir.Close(); err != nil {
return err
}
}
@@ -1236,6 +974,9 @@ func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decomp
if options == nil {
options = &TarOptions{}
}
if options.ExcludePatterns == nil {
options.ExcludePatterns = []string{}
}
r := tarArchive
if decompress {
@@ -1243,7 +984,7 @@ func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decomp
if err != nil {
return err
}
defer func() { _ = decompressedArchive.Close() }()
defer decompressedArchive.Close()
r = decompressedArchive
}
@@ -1257,7 +998,7 @@ func (archiver *Archiver) TarUntar(src, dst string) error {
if err != nil {
return err
}
defer func() { _ = archive.Close() }()
defer archive.Close()
return archiver.Untar(archive, dst, &TarOptions{
IDMap: archiver.IDMapping,
})
@@ -1269,7 +1010,7 @@ func (archiver *Archiver) UntarPath(src, dst string) error {
if err != nil {
return err
}
defer func() { _ = archive.Close() }()
defer archive.Close()
return archiver.Untar(archive, dst, &TarOptions{
IDMap: archiver.IDMapping,
})
@@ -1329,13 +1070,13 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
defer close(errC)
errC <- func() error {
defer func() { _ = w.Close() }()
defer w.Close()
srcF, err := os.Open(src)
if err != nil {
return err
}
defer func() { _ = srcF.Close() }()
defer srcF.Close()
hdr, err := tarheader.FileInfoHeaderNoLookups(srcSt, "")
if err != nil {
@@ -1346,14 +1087,14 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
hdr.AccessTime = time.Time{}
hdr.ChangeTime = time.Time{}
hdr.Name = filepath.Base(dst)
hdr.Mode = chmodTarEntry(hdr.Mode)
hdr.Mode = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
if err := remapIDs(archiver.IDMapping, hdr); err != nil {
return err
}
tw := tar.NewWriter(w)
defer func() { _ = tw.Close() }()
defer tw.Close()
if err := tw.WriteHeader(hdr); err != nil {
return err
}
@@ -1371,7 +1112,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
err = archiver.Untar(r, filepath.Dir(dst), nil)
if err != nil {
_ = r.CloseWithError(err)
r.CloseWithError(err)
}
return err
}
+38 -91
View File
@@ -4,71 +4,45 @@ import (
"archive/tar"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/moby/go-archive/internal/archiveoptions"
"github.com/moby/sys/userns"
"golang.org/x/sys/unix"
)
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
procSelfFD, err := os.Open("/proc/self/fd")
if err != nil {
return nil, nil, err
}
var prepared TarOptions
if opts != nil {
prepared = *opts
}
prepared.internalOptions = &archiveoptions.Options{
ProcSelfFD: procSelfFD,
}
return &prepared, func() { _ = procSelfFD.Close() }, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
if format == OverlayWhiteoutFormat {
return newOverlayWhiteoutConverter()
return overlayWhiteoutConverter{}
}
return nil
}
type overlayWhiteoutConverter struct {
opaqueXattr string
}
type overlayWhiteoutConverter struct{}
func newOverlayWhiteoutConverter() overlayWhiteoutConverter {
opaqueXattr := "trusted.overlay.opaque"
if userns.RunningInUserNS() {
opaqueXattr = "user.overlay.opaque"
}
return overlayWhiteoutConverter{
opaqueXattr: opaqueXattr,
}
}
func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string, fi os.FileInfo) (wo *tar.Header, _ error) {
func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, _ error) {
// convert whiteouts to AUFS format
if fi.Mode()&os.ModeCharDevice != 0 && hdr.Devmajor == 0 && hdr.Devminor == 0 {
// we just rename the file and make it normal
dir, filename := path.Split(hdr.Name)
hdr.Name = path.Join(dir, WhiteoutPrefix+filename)
dir, filename := filepath.Split(hdr.Name)
hdr.Name = filepath.Join(dir, WhiteoutPrefix+filename)
hdr.Mode = 0o600
hdr.Typeflag = tar.TypeReg
hdr.Size = 0
}
if !fi.IsDir() {
if fi.Mode()&os.ModeDir == 0 {
// FIXME(thaJeztah): return a sentinel error instead of nil, nil
return nil, nil
}
opaqueXattrName := "trusted.overlay.opaque"
if userns.RunningInUserNS() {
opaqueXattrName = "user.overlay.opaque"
}
// convert opaque dirs to AUFS format by writing an empty file with the prefix
opaque, err := lgetxattr(filePath, c.opaqueXattr)
opaque, err := lgetxattr(path, opaqueXattrName)
if err != nil {
return nil, err
}
@@ -76,14 +50,14 @@ func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string,
// FIXME(thaJeztah): return a sentinel error instead of nil, nil
return nil, nil
}
delete(hdr.PAXRecords, paxSchilyXattr+c.opaqueXattr)
delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName)
// create a header for the whiteout file
// it should inherit some properties from the parent, but be a regular file
return &tar.Header{
Typeflag: tar.TypeReg,
Mode: hdr.Mode & int64(os.ModePerm),
Name: path.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
Name: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
Size: 0,
Uid: hdr.Uid,
Uname: hdr.Uname,
@@ -94,67 +68,40 @@ func (c overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, filePath string,
}, nil
}
func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, filePath string) (bool, error) {
base := filepath.Base(filePath)
dir := filepath.Dir(filePath)
func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) {
base := filepath.Base(path)
dir := filepath.Dir(path)
switch base {
case WhiteoutPrefix, WhiteoutPrefix + ".", WhiteoutPrefix + "..":
return false, fmt.Errorf("invalid whiteout entry %q", hdr.Name)
// if a directory is marked as opaque by the AUFS special file, we need to translate that to overlay
if base == WhiteoutOpaqueDir {
opaqueXattrName := "trusted.overlay.opaque"
if userns.RunningInUserNS() {
opaqueXattrName = "user.overlay.opaque"
}
case WhiteoutOpaqueDir:
parent, err := root.Open(dir)
err := unix.Setxattr(dir, opaqueXattrName, []byte{'y'}, 0)
if err != nil {
return false, err
return false, fmt.Errorf("setxattr('%s', %s=y): %w", dir, opaqueXattrName, err)
}
defer parent.Close()
// don't write the file itself
return false, err
}
// If a directory is marked as opaque by the AUFS special file, we need to translate that to overlay.
if err := unix.Fsetxattr(int(parent.Fd()), c.opaqueXattr, []byte{'y'}, 0); err != nil {
return false, fmt.Errorf("fsetxattr('%s', %s=y): %w", dir, c.opaqueXattr, err)
}
// Don't write the whiteout file itself.
return false, nil
default:
originalBase, ok := strings.CutPrefix(base, WhiteoutPrefix)
if !ok {
// Regular file.
return true, nil
}
parent, err := root.Open(dir)
if err != nil {
return false, err
}
defer parent.Close()
// If a file was deleted, and we are using overlay, we need to create a character device.
// if a file was deleted and we are using overlay, we need to create a character device
if strings.HasPrefix(base, WhiteoutPrefix) {
originalBase := base[len(WhiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)
if err := unix.Mknodat(int(parent.Fd()), originalBase, unix.S_IFCHR, 0); err != nil {
if err := unix.Mknod(originalPath, unix.S_IFCHR, 0); err != nil {
return false, fmt.Errorf("failed to mknod('%s', S_IFCHR, 0): %w", originalPath, err)
}
// Header IDs have already been remapped. Optimize the common non-remapped
// root-owned (0:0) case by assuming the created whiteout has the expected
// ownership, rather than comparing against the effective UID/GID or stat'ing
// the created node to verify it.
if hdr.Uid != 0 || hdr.Gid != 0 {
// TODO(thaJeztah): Revisit whether whiteout ownership needs to be preserved.
//
// This was added in the original overlay whiteout implementation:
// https://github.com/moby/moby/pull/18560 / https://github.com/moby/moby/pull/22126
//
// OverlayFS documents whiteouts in terms of a character device with device
// number 0:0, not ownership: https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories
//
// If ownership is not required, this Fchownat can be removed to avoid the remaining TOCTOU window.
if err := unix.Fchownat(int(parent.Fd()), originalBase, hdr.Uid, hdr.Gid, unix.AT_SYMLINK_NOFOLLOW); err != nil {
return false, &os.PathError{Op: "lchown", Path: originalPath, Err: err}
}
if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil {
return false, err
}
// Don't write the whiteout file itself.
// don't write the file itself
return false, nil
}
return true, nil
}

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