Compare commits

..
23 Commits
Author SHA1 Message Date
Paweł GronowskiandGitHub 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 StijnandGitHub 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 StijnandGitHub 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ł GronowskiandGitHub 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 StijnandGitHub 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ł GronowskiandGitHub 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]andSebastiaan van Stijn 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]andSebastiaan van Stijn 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ł GronowskiandGitHub 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
196 changed files with 2183 additions and 4758 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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.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@a492c6d04fd3315f67230809b44d60cc0acd50b3 # v1.16.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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.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@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: go
-
name: Autobuild
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
-
name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:go"
+2 -2
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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
-
name: Run ${{ matrix.target }}
run: |
-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
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@baf90543d81f5de59751dfd10e6cf45e21a5a982 # v2.0.3
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
+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: |
+5 -5
View File
@@ -30,10 +30,10 @@ jobs:
steps:
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.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
-
@@ -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
+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
@@ -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))
}
+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)"
+13 -13
View File
@@ -21,7 +21,7 @@ require (
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.0
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
@@ -31,9 +31,9 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/mattn/go-runewidth v0.0.24
github.com/moby/go-archive v0.3.3
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,7 +46,7 @@ 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/sirupsen/logrus v1.10.1
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
@@ -59,11 +59,11 @@ require (
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.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.40.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
)
@@ -87,7 +87,7 @@ require (
github.com/gorilla/mux v1.8.1 // 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.1.0 // indirect
@@ -103,11 +103,11 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // 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.38.0 // indirect
golang.org/x/net v0.57.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-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
+37 -32
View File
@@ -32,6 +32,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
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=
@@ -41,8 +42,8 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi
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.0 h1:T9UlP76qPLA/HaLrcC+s4Doqqv5XsWMMUGPF5Aih/k0=
github.com/docker/go-connections v0.8.0/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.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=
@@ -95,10 +96,14 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
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.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
@@ -106,12 +111,12 @@ github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhg
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=
@@ -120,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=
@@ -152,6 +153,7 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr
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=
@@ -171,11 +173,13 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
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/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
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=
@@ -185,8 +189,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
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.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
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=
@@ -226,32 +230,31 @@ go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpu
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.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
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=
@@ -260,14 +263,14 @@ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7w
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.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
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=
@@ -284,12 +287,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:
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.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
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=
+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
}
-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
}
-39
View File
@@ -1,39 +0,0 @@
//go:build !linux && !windows
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)
}
+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)
}
-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
}
-8
View File
@@ -2,14 +2,6 @@
package archive
func withProcSelfFD(opts *TarOptions) (*TarOptions, func(), error) {
var prepared TarOptions
if opts != nil {
prepared = *opts
}
return &prepared, func() {}, nil
}
func getWhiteoutConverter(format WhiteoutFormat) tarWhiteoutConverter {
return nil
}
+19 -81
View File
@@ -5,19 +5,14 @@ package archive
import (
"archive/tar"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
var errInvalidArchive = errors.New("invalid archive")
// addLongPathPrefix adds the Windows long path prefix to the path provided if
// it does not already have it. It is a no-op on platforms other than Windows.
func addLongPathPrefix(srcPath string) string {
@@ -34,19 +29,20 @@ func getWalkRoot(srcPath string, include string) string {
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(mode int64) int64 {
return mode // noop for unix as golang APIs provide perm bits correctly
func chmodTarEntry(perm os.FileMode) os.FileMode {
return perm // noop for unix as golang APIs provide perm bits correctly
}
func getInodeFromStat(stat any) (uint64, error) {
func getInodeFromStat(stat interface{}) (uint64, error) {
s, ok := stat.(*syscall.Stat_t)
if !ok {
return 0, fmt.Errorf("unexpected stat type %T", stat)
// FIXME(thaJeztah): this should likely return an error; see https://github.com/moby/moby/pull/49493#discussion_r1979152897
return 0, nil
}
return s.Ino, nil
}
func getFileUIDGID(stat any) (int, int, error) {
func getFileUIDGID(stat interface{}) (int, int, error) {
s, ok := stat.(*syscall.Stat_t)
if !ok {
@@ -60,7 +56,7 @@ func getFileUIDGID(stat any) (int, int, error) {
//
// Creating device nodes is not supported when running in a user namespace,
// produces a [syscall.EPERM] in most cases.
func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string) error {
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
mode := uint32(hdr.Mode & 0o7777)
switch hdr.Typeflag {
case tar.TypeBlock:
@@ -71,78 +67,20 @@ func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, dstPath string)
mode |= unix.S_IFIFO
}
// Devmajor and Devminor come straight from the (untrusted) tar header as
// int64, but Mkdev only takes uint32. Casting a value that does not fit
// silently truncates it, so the node created on disk would carry a
// different major/minor than the header declares. Reject those instead of
// creating a mismatched device.
if hdr.Devmajor < 0 || hdr.Devmajor > math.MaxUint32 ||
hdr.Devminor < 0 || hdr.Devminor > math.MaxUint32 {
return fmt.Errorf("device number %d:%d for %q out of range: %w", hdr.Devmajor, hdr.Devminor, hdr.Name, errInvalidArchive)
}
// Prefer mknodat; fall back to a bounded path where unavailable.
return mknodInRoot(root, dstPath, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor)))
return mknod(path, mode, unix.Mkdev(uint32(hdr.Devmajor), uint32(hdr.Devminor)))
}
// handleLChmod applies the mode from hdrInfo to dstPath within root, skipping
// symlinks (there is no lchmod). For hardlinks, the mode is applied only when
// the link target is itself not a symlink.
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts *archiveoptions.Options) error {
switch hdr.Typeflag {
case tar.TypeSymlink:
return nil
case tar.TypeLink:
// If the target is a symlink, there is no way to chmod the hardlink
// without following it.
fi, err := root.Lstat(hardlinkTarget)
if err != nil || fi.Mode()&os.ModeSymlink != 0 {
return nil
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
if hdr.Typeflag == tar.TypeLink {
if fi, err := os.Lstat(hdr.Linkname); err == nil && (fi.Mode()&os.ModeSymlink == 0) {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
}
} else if hdr.Typeflag != tar.TypeSymlink {
if err := os.Chmod(path, hdrInfo.Mode()); err != nil {
return err
}
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
default:
return chmodNoSymlink(root, dstPath, hdrInfo.Mode(), opts)
}
}
// chmodNoSymlink applies mode to a non-symlink entry.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlink(root *os.Root, name string, mode os.FileMode, opts *archiveoptions.Options) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
if err != nil {
return err
}
defer parent.Close()
base := filepath.Base(name)
perm := fileModeToPerm(mode)
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
if err := unix.Fchmodat(int(parent.Fd()), base, perm, unix.AT_SYMLINK_NOFOLLOW); err == nil {
return nil
} else if !errors.Is(err, syscall.EOPNOTSUPP) {
return &os.PathError{Op: "fchmodat2", Path: name, Err: err}
}
// Fallback for systems that cannot perform fchmodat with AT_SYMLINK_NOFOLLOW.
return chmodNoSymlinkFallback(int(parent.Fd()), base, name, perm, opts) // #nosec G115 -- ignore integer overflow conversion for parent.Fd
}
// fileModeToPerm returns the subset of an os.FileMode that can be applied
// by chmod.
func fileModeToPerm(mode os.FileMode) uint32 {
perm := uint32(mode.Perm())
if mode&os.ModeSetuid != 0 {
perm |= unix.S_ISUID
}
if mode&os.ModeSetgid != 0 {
perm |= unix.S_ISGID
}
if mode&os.ModeSticky != 0 {
perm |= unix.S_ISVTX
}
return perm
return nil
}
+7 -8
View File
@@ -33,31 +33,30 @@ func getWalkRoot(srcPath string, include string) string {
// chmodTarEntry is used to adjust the file permissions used in tar header based
// on the platform the archival is done.
func chmodTarEntry(mode int64) int64 {
func chmodTarEntry(perm os.FileMode) os.FileMode {
// Remove group- and world-writable bits.
mode &= 0o755
perm &= 0o755
// Add the x bit: make everything +x on Windows
return mode | 0o111
return perm | 0o111
}
func getInodeFromStat(stat any) (uint64, error) {
func getInodeFromStat(stat interface{}) (uint64, error) {
// do nothing. no notion of Inode in stat on Windows
return 0, nil
}
// handleTarTypeBlockCharFifo is an OS-specific helper function used by
// createTarFile to handle the following types of header: Block; Char; Fifo
func handleTarTypeBlockCharFifo(root *os.Root, hdr *tar.Header, path string) error {
func handleTarTypeBlockCharFifo(hdr *tar.Header, path string) error {
return nil
}
// handleLChmod is a no-op on Windows because chmod is not supported.
func handleLChmod(root *os.Root, dstPath string, hardlinkTarget string, hdr *tar.Header, hdrInfo os.FileInfo, opts any) error {
func handleLChmod(hdr *tar.Header, path string, hdrInfo os.FileInfo) error {
return nil
}
func getFileUIDGID(stat any) (int, int, error) {
func getFileUIDGID(stat interface{}) (int, int, error) {
// no notion of file ownership mapping yet on Windows
return 0, 0, nil
}
+9 -9
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"io"
"io/fs"
"maps"
"os"
"path/filepath"
"sort"
@@ -218,8 +217,8 @@ func (info *FileInfo) LookUp(path string) *FileInfo {
return info
}
pathElements := strings.SplitSeq(path, string(os.PathSeparator))
for elem := range pathElements {
pathElements := strings.Split(path, string(os.PathSeparator))
for _, elem := range pathElements {
if elem != "" {
child := parent.children[elem]
if child == nil {
@@ -257,7 +256,9 @@ func (info *FileInfo) addChanges(oldInfo *FileInfo, changes *[]Change) {
// otherwise any previous delete/change is considered recursive
oldChildren := make(map[string]*FileInfo)
if oldInfo != nil && info.isDir() {
maps.Copy(oldChildren, oldInfo.children)
for k, v := range oldInfo.children {
oldChildren[k] = v
}
}
for name, newChild := range info.children {
@@ -400,7 +401,7 @@ func ExportChanges(dir string, changes []Change, idMap user.IdentityMapping) (io
whiteOut := filepath.Join(whiteOutDir, WhiteoutPrefix+whiteOutBase)
timestamp := time.Now()
hdr := &tar.Header{
Name: strings.TrimPrefix(filepath.ToSlash(whiteOut), "/"),
Name: whiteOut[1:],
Size: 0,
ModTime: timestamp,
AccessTime: timestamp,
@@ -410,10 +411,9 @@ func ExportChanges(dir string, changes []Change, idMap user.IdentityMapping) (io
log.G(context.TODO()).Debugf("Can't write whiteout header: %s", err)
}
} else {
srcPath := filepath.Join(dir, change.Path)
archivePath := strings.TrimPrefix(filepath.ToSlash(change.Path), "/")
if err := ta.addTarFile(srcPath, archivePath); err != nil {
log.G(context.TODO()).Debugf("Can't add file %s to tar: %s", srcPath, err)
path := filepath.Join(dir, change.Path)
if err := ta.addTarFile(path, change.Path[1:]); err != nil {
log.G(context.TODO()).Debugf("Can't add file %s to tar: %s", path, err)
}
}
}
+1 -1
View File
@@ -265,7 +265,7 @@ func parseDirent(buf []byte, names []nameIno) (consumed int, newnames []nameIno)
}
func clen(n []byte) int {
for i := range n {
for i := 0; i < len(n); i++ {
if n[i] == 0 {
return i
}
+1 -1
View File
@@ -26,7 +26,7 @@ func collectFileInfoForChanges(oldDir, newDir string) (*FileInfo, *FileInfo, err
}()
// block until both routines have returned
for range 2 {
for i := 0; i < 2; i++ {
if err := <-errs; err != nil {
return nil, nil, err
}
-46
View File
@@ -1,46 +0,0 @@
package archive
import (
"fmt"
"os"
"runtime"
"strconv"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, opts *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_PATH|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if opts != nil && opts.ProcSelfFD != nil {
err := unix.Fchmodat(int(opts.ProcSelfFD.Fd()), strconv.Itoa(fd), perm, 0)
// Keep the os.File alive until fchmodat has finished using its descriptor.
runtime.KeepAlive(opts.ProcSelfFD)
if err != nil {
return &os.PathError{
Op: "fchmodat",
Path: name,
Err: fmt.Errorf("via pre-opened /proc/self/fd/%d: %w", fd, err),
}
}
} else {
procPath := "/proc/self/fd/" + strconv.Itoa(fd)
if err := unix.Chmod(procPath, perm); err != nil {
return &os.PathError{
Op: "chmod",
Path: name,
Err: fmt.Errorf("via %s: %w", procPath, err),
}
}
}
return nil
}
-27
View File
@@ -1,27 +0,0 @@
//go:build !linux && !windows
package archive
import (
"os"
"github.com/moby/go-archive/internal/archiveoptions"
"golang.org/x/sys/unix"
)
// chmodNoSymlinkFallback applies mode without following the final path
// component on systems without fchmodat2 support.
//
// Callers must have already excluded symlink entries.
func chmodNoSymlinkFallback(parentFD int, base, name string, perm uint32, _ *archiveoptions.Options) error {
fd, err := unix.Openat(parentFD, base, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK|unix.O_CLOEXEC, 0)
if err != nil {
return &os.PathError{Op: "openat", Path: name, Err: err}
}
defer unix.Close(fd)
if err := unix.Fchmod(fd, perm); err != nil {
return &os.PathError{Op: "fchmod", Path: name, Err: err}
}
return nil
}
+2 -2
View File
@@ -66,7 +66,7 @@ type nopWriteCloser struct {
func (nopWriteCloser) Close() error { return nil }
var bufioReader32KPool = &sync.Pool{
New: func() any { return bufio.NewReaderSize(nil, 32*1024) },
New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
}
type bufferedReader struct {
@@ -217,7 +217,7 @@ func gzipDecompress(ctx context.Context, buf io.Reader) (io.ReadCloser, error) {
log.G(ctx).Debugf("Using %s to decompress", unpigzPath)
return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf) // #nosec G204 -- Subprocess launched with variable
return cmdStream(exec.CommandContext(ctx, unpigzPath, "-d", "-c"), buf)
}
// cmdStream executes a command, and returns its stdout as a stream.
+21 -43
View File
@@ -22,7 +22,7 @@ var (
)
var copyPool = sync.Pool{
New: func() any { s := make([]byte, 32*1024); return &s },
New: func() interface{} { s := make([]byte, 32*1024); return &s },
}
func copyWithBuffer(dst io.Writer, src io.Reader) error {
@@ -316,40 +316,16 @@ func PrepareArchiveCopy(srcContent io.Reader, srcInfo, dstInfo CopyInfo) (dstDir
}
}
// newNameRebaser returns a function that replaces oldBase with newBase at the
// beginning of POSIX-style archive entry names. It converts oldBase and newBase
// to forward-slash form and trims trailing slashes.
//
// When rebasing from the archive root, the returned function removes all
// leading slashes from names. It otherwise preserves the remainder verbatim
// and does not clean or canonicalize paths.
func newNameRebaser(oldBase, newBase string) func(string) string {
oldBase = strings.TrimRight(filepath.ToSlash(oldBase), "/")
newBase = strings.TrimRight(filepath.ToSlash(newBase), "/")
if oldBase == "" {
return func(name string) string {
name = strings.TrimLeft(name, "/")
if newBase == "" {
return name
}
return newBase + "/" + name
}
}
return func(name string) string {
suffix, ok := strings.CutPrefix(name, oldBase)
if !ok || suffix != "" && !strings.HasPrefix(suffix, "/") {
return name
}
return newBase + suffix
}
}
// RebaseArchiveEntries rewrites the given srcContent archive replacing
// an occurrence of oldBase with newBase at the beginning of entry names.
func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.ReadCloser {
rebase := newNameRebaser(oldBase, newBase)
if oldBase == string(os.PathSeparator) {
// If oldBase specifies the root directory, use an empty string as
// oldBase instead so that newBase doesn't replace the path separator
// that all paths will start with.
oldBase = ""
}
rebased, w := io.Pipe()
go func() {
@@ -360,12 +336,12 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read
hdr, err := srcTar.Next()
if errors.Is(err, io.EOF) {
// Signals end of archive.
_ = rebasedTar.Close()
_ = w.Close()
rebasedTar.Close()
w.Close()
return
}
if err != nil {
_ = w.CloseWithError(err)
w.CloseWithError(err)
return
}
@@ -377,13 +353,13 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read
//
// To fix, set the format to PAX here. See docker/for-linux issue #484.
hdr.Format = tar.FormatPAX
hdr.Name = rebase(hdr.Name)
hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
if hdr.Typeflag == tar.TypeLink {
hdr.Linkname = rebase(hdr.Linkname)
hdr.Linkname = strings.Replace(hdr.Linkname, oldBase, newBase, 1)
}
if err = rebasedTar.WriteHeader(hdr); err != nil {
_ = w.CloseWithError(err)
w.CloseWithError(err)
return
}
@@ -398,7 +374,7 @@ func RebaseArchiveEntries(srcContent io.Reader, oldBase, newBase string) io.Read
// not be vulnerable to this code consuming memory.
//nolint:gosec // G110: Potential DoS vulnerability via decompression bomb (gosec)
if _, err = io.Copy(rebasedTar, srcTar); err != nil {
_ = w.CloseWithError(err)
w.CloseWithError(err)
return
}
}
@@ -432,7 +408,7 @@ func CopyResource(srcPath, dstPath string, followLink bool) error {
if err != nil {
return err
}
defer func() { _ = content.Close() }()
defer content.Close()
return CopyTo(content, srcInfo, dstPath)
}
@@ -451,12 +427,14 @@ func CopyTo(content io.Reader, srcInfo CopyInfo, dstPath string) error {
if err != nil {
return err
}
defer func() { _ = copyArchive.Close() }()
defer copyArchive.Close()
return Untar(copyArchive, dstDir, &TarOptions{
options := &TarOptions{
NoLchown: true,
NoOverwriteDirNonDir: true,
})
}
return Untar(copyArchive, dstDir, options)
}
// ResolveHostSourcePath decides real path need to be copied with parameters such as
-21
View File
@@ -1,21 +0,0 @@
//go:build darwin
package archive
import (
"os"
"golang.org/x/sys/unix"
)
func mknod(path string, mode uint32, dev uint64) error {
return unix.Mknod(path, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API.
}
func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error {
abs, err := fsRootPath(root.Name(), path)
if err != nil {
return err
}
return unix.Mknod(abs, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API.
}
+1 -16
View File
@@ -2,23 +2,8 @@
package archive
import (
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
import "golang.org/x/sys/unix"
func mknod(path string, mode uint32, dev uint64) error {
return unix.Mknod(path, mode, dev)
}
func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error {
parent, err := root.OpenFile(filepath.Dir(path), os.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return err
}
defer parent.Close()
return unix.Mknodat(int(parent.Fd()), filepath.Base(path), mode, dev)
}
+3 -18
View File
@@ -1,24 +1,9 @@
//go:build !darwin && !freebsd && !windows
//go:build !windows && !freebsd
package archive
import (
"os"
"path/filepath"
"golang.org/x/sys/unix"
)
import "golang.org/x/sys/unix"
func mknod(path string, mode uint32, dev uint64) error {
return unix.Mknod(path, mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API.
}
func mknodInRoot(root *os.Root, path string, mode uint32, dev uint64) error {
parent, err := root.OpenFile(filepath.Dir(path), os.O_RDONLY|unix.O_DIRECTORY, 0)
if err != nil {
return err
}
defer parent.Close()
return unix.Mknodat(int(parent.Fd()), filepath.Base(path), mode, int(dev)) // #nosec G115 -- Required conversion for the platform-specific Mknod API.
return unix.Mknod(path, mode, int(dev))
}
+64 -79
View File
@@ -7,8 +7,8 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/containerd/log"
@@ -20,23 +20,17 @@ import (
// compressed or uncompressed.
// Returns the size in bytes of the contents of the layer.
func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, err error) {
root, err := os.OpenRoot(dest)
if err != nil {
return 0, err
}
defer root.Close()
tr := tar.NewReader(layer)
var dirs []unpackedDir
// unpackedPaths tracks resolved, native-separator, root-relative paths
// already written in this layer so that the AUFS opaque-whiteout walk
// knows which paths to preserve.
var dirs []*tar.Header
unpackedPaths := make(map[string]struct{})
if options == nil {
options = &TarOptions{}
}
if options.ExcludePatterns == nil {
options.ExcludePatterns = []string{}
}
aufsTempdir := ""
aufsHardlinks := make(map[string]*tar.Header)
@@ -54,22 +48,34 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
size += hdr.Size
// 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 0, breakoutError(fmt.Errorf("invalid entry name %q", hdr.Name))
}
hdr.Name = name
// Normalize name, for safety and for a simple is-root check
hdr.Name = filepath.Clean(hdr.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
// Windows does not support filenames with colons in them. Ignore
// these files. This is not a problem though (although it might
// appear that it is). Let's suppose a client is running docker pull.
// The daemon it points to is Windows. Would it make sense for the
// client to be doing a docker pull Ubuntu for example (which has files
// with colons in the name under /usr/share/man/man3)? No, absolutely
// not as it would really only make sense that they were pulling a
// Windows image. However, for development, it is necessary to be able
// to pull Linux images which are in the repository.
//
// TODO Windows. Once the registry is aware of what images are Windows-
// specific or Linux-specific, this warning should be changed to an error
// to cater for the situation where someone does manage to upload a Linux
// image but have it tagged as Windows inadvertently.
if runtime.GOOS == "windows" {
if strings.Contains(hdr.Name, ":") {
log.G(context.TODO()).Warnf("Windows: Ignoring %s (is this a Linux image?)", hdr.Name)
continue
}
}
// Ensure that the parent directory exists.
err = createImpliedDirectories(dest, hdr, options)
if err != nil {
return 0, err
}
// Skip AUFS metadata dirs
@@ -78,7 +84,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
// We don't want this directory, but we need the files in them so that
// such hardlinks can be resolved.
if strings.HasPrefix(hdr.Name, WhiteoutLinkDir) && hdr.Typeflag == tar.TypeReg {
basename := path.Base(hdr.Name)
basename := filepath.Base(hdr.Name)
aufsHardlinks[basename] = hdr
if aufsTempdir == "" {
if aufsTempdir, err = os.MkdirTemp(dest, "dockerplnk"); err != nil {
@@ -86,68 +92,47 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
}
defer os.RemoveAll(aufsTempdir)
}
aufsRoot, err := os.OpenRoot(aufsTempdir)
if err != nil {
if err := createTarFile(filepath.Join(aufsTempdir, basename), dest, hdr, tr, options); err != nil {
return 0, err
}
cerr := createTarFile(aufsRoot, basename, hdr, tr, options)
_ = aufsRoot.Close()
if cerr != nil {
return 0, cerr
}
}
if hdr.Name != WhiteoutOpaqueDir {
continue
}
}
// dstPath is the native (host-separator) form of the entry name,
// used at all filesystem boundaries (os.Root methods, fsRootPath).
// The tar-header name (hdr.Name) is POSIX, so convert it here.
dstPath, err := resolveArchivePath(root, filepath.FromSlash(hdr.Name))
// #nosec G305 -- The joined path is guarded against path traversal.
path := filepath.Join(dest, hdr.Name)
rel, err := filepath.Rel(dest, path)
if err != nil {
return 0, err
}
// Ensure that the parent directory exists.
if err := createImpliedDirectories(root, dstPath, options); err != nil {
return 0, err
// Note as these operations are platform specific, so must the slash be.
if strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return 0, breakoutError(fmt.Errorf("%q is outside of %q", hdr.Name, dest))
}
if base := filepath.Base(dstPath); strings.HasPrefix(base, WhiteoutPrefix) {
dir := filepath.Dir(dstPath)
base := filepath.Base(path)
if strings.HasPrefix(base, WhiteoutPrefix) {
dir := filepath.Dir(path)
if base == WhiteoutOpaqueDir {
_, err := root.Lstat(dir)
_, err := os.Lstat(dir)
if err != nil {
return 0, err
}
// Walk the absolute directory so we can call os.RemoveAll on
// paths outside the walk callback's reach, then convert each
// walked path back to a root-relative name for the
// unpackedPaths check.
// fsRootPath walks each path component and bounds any symlinks
// within the root to prevent TOCTOU symlink attacks.
absDir, err := fsRootPath(root.Name(), dir)
if err != nil {
return 0, err
}
err = filepath.WalkDir(absDir, func(p string, info os.DirEntry, err error) error {
err = filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil // parent was deleted
err = nil // parent was deleted
}
return err
}
if p == absDir {
if path == dir {
return nil
}
rel, err := filepath.Rel(root.Name(), p)
if err != nil {
return err
}
// unpackedPaths is keyed by resolved, native-separator,
// root-relative paths, matching filepath.WalkDir's paths.
if _, exists := unpackedPaths[rel]; !exists {
return root.RemoveAll(rel)
if _, exists := unpackedPaths[path]; !exists {
return os.RemoveAll(path)
}
return nil
})
@@ -157,18 +142,18 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
} else {
originalBase := base[len(WhiteoutPrefix):]
originalPath := filepath.Join(dir, originalBase)
if err := root.RemoveAll(originalPath); err != nil {
if err := os.RemoveAll(originalPath); err != nil {
return 0, err
}
}
} else {
// If dstPath exists we almost always just want to remove and replace it.
// 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 !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
if err := root.RemoveAll(dstPath); err != nil {
if err := os.RemoveAll(path); err != nil {
return 0, err
}
}
@@ -179,8 +164,8 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
// Hard links into /.wh..wh.plnk don't work, as we don't extract that directory, so
// we manually retarget these into the temporary files we extracted them into
if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(path.Clean(hdr.Linkname), WhiteoutLinkDir) {
linkBasename := path.Base(hdr.Linkname)
if hdr.Typeflag == tar.TypeLink && strings.HasPrefix(filepath.Clean(hdr.Linkname), WhiteoutLinkDir) {
linkBasename := filepath.Base(hdr.Linkname)
srcHdr = aufsHardlinks[linkBasename]
if srcHdr == nil {
return 0, errors.New("invalid aufs hardlink")
@@ -197,23 +182,23 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64,
return 0, err
}
if err := createTarFile(root, dstPath, srcHdr, srcData, options); err != nil {
if err := createTarFile(path, dest, srcHdr, srcData, options); err != nil {
return 0, 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)
}
// Record the resolved, native-separator, root-relative path so it
// matches the paths produced by the opaque-whiteout walk.
unpackedPaths[dstPath] = struct{}{}
unpackedPaths[path] = struct{}{}
}
}
for _, d := range dirs {
if err := chtimes(root, d.name, boundTime(latestTime(d.hdr.AccessTime, d.hdr.ModTime)), 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, hdr.AccessTime, hdr.ModTime); err != nil {
return 0, err
}
}
-12
View File
@@ -1,12 +0,0 @@
// Package archiveoptions defines internal options shared between archive and
// chrootarchive.
package archiveoptions
import "os"
// Options contains extraction resources supplied by internal callers.
type Options struct {
// ProcSelfFD references /proc/self/fd as opened before entering a chroot.
// The caller retains ownership of the file.
ProcSelfFD *os.File
}
-142
View File
@@ -1,142 +0,0 @@
/*
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package archive
import (
"errors"
"os"
"path/filepath"
)
var errTooManyLinks = errors.New("too many links")
type fsRootPathResult struct {
path string
followedAbsoluteLink bool
relativeEscapeBeforeAbsolute bool
}
// fsRootPath joins a path with a root, evaluating and bounding any
// symlink to the root directory.
func fsRootPath(root, path string) (string, error) {
result, err := resolveFSRootPath(root, path)
if err != nil {
return "", err
}
return result.path, nil
}
func resolveFSRootPath(root, path string) (fsRootPathResult, error) {
result := fsRootPathResult{path: root}
if path == "" {
return result, nil
}
var linksWalked int // to protect against cycles
for {
i := linksWalked
newpath, err := walkLinks(root, path, &linksWalked, &result)
if err != nil {
return fsRootPathResult{}, err
}
path = newpath
if i == linksWalked {
newpath = filepath.Join(string(os.PathSeparator), newpath)
if path == newpath {
result.path = filepath.Join(root, newpath)
return result, nil
}
path = newpath
}
}
}
func walkLink(root, path string, linksWalked *int, result *fsRootPathResult) (newpath string, islink bool, err error) {
if *linksWalked > 255 {
return "", false, errTooManyLinks
}
path = filepath.Join(string(os.PathSeparator), path)
if path == string(os.PathSeparator) {
return path, false, nil
}
realPath := filepath.Join(root, path)
fi, err := os.Lstat(realPath)
if err != nil {
// If path does not yet exist, treat as non-symlink
if os.IsNotExist(err) {
return path, false, nil
}
return "", false, err
}
if fi.Mode()&os.ModeSymlink == 0 {
return path, false, nil
}
newpath, err = os.Readlink(realPath)
if err != nil {
return "", false, err
}
if filepath.IsAbs(newpath) {
result.followedAbsoluteLink = true
} else if !result.followedAbsoluteLink {
// Record an escape before a later absolute link can make the original
// os.Root error appear eligible for resolve-in-root fallback.
relativeDir, err := filepath.Rel(string(os.PathSeparator), filepath.Dir(path))
if err != nil {
return "", false, err
}
resolved := filepath.Join(relativeDir, newpath)
if resolved != "." && !filepath.IsLocal(resolved) {
result.relativeEscapeBeforeAbsolute = true
}
}
*linksWalked++
return newpath, true, nil
}
func walkLinks(root, path string, linksWalked *int, result *fsRootPathResult) (string, error) {
switch dir, file := filepath.Split(path); {
case dir == "":
newpath, _, err := walkLink(root, file, linksWalked, result)
return newpath, err
case file == "":
if os.IsPathSeparator(dir[len(dir)-1]) {
if dir == string(os.PathSeparator) {
return dir, nil
}
return walkLinks(root, dir[:len(dir)-1], linksWalked, result)
}
newpath, _, err := walkLink(root, dir, linksWalked, result)
return newpath, err
default:
newdir, err := walkLinks(root, dir, linksWalked, result)
if err != nil {
return "", err
}
newpath, islink, err := walkLink(root, filepath.Join(newdir, file), linksWalked, result)
if err != nil {
return "", err
}
if !islink || filepath.IsAbs(newpath) {
return newpath, nil
}
return filepath.Join(newdir, newpath), nil
}
}
-6
View File
@@ -1,6 +0,0 @@
//go:build !windows
package archive
// windows_O_FILE_FLAG_SEQUENTIAL_SCAN is not supported on go < 1.26.
const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0
-9
View File
@@ -1,9 +0,0 @@
//go:build windows && go1.26
package archive
// windows_O_FILE_FLAG_SEQUENTIAL_SCAN matches [golang.org/x/sys/windows.O_FILE_FLAG_SEQUENTIAL_SCAN].
// Starting in Go 1.26, os.OpenFile supports passing this flag through.
//
// TODO(thaJeztah): use windows.O_FILE_FLAG_SEQUENTIAL_SCAN once we drop Go <1.26.
const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0x08000000
-6
View File
@@ -1,6 +0,0 @@
//go:build windows && !go1.26
package archive
// windows_O_FILE_FLAG_SEQUENTIAL_SCAN is not supported on go < 1.26.
const windows_O_FILE_FLAG_SEQUENTIAL_SCAN = 0
+1 -1
View File
@@ -32,7 +32,7 @@ func (fi nosysFileInfo) Gname() (string, error) {
return "", nil
}
func (fi nosysFileInfo) Sys() any {
func (fi nosysFileInfo) Sys() interface{} {
// A Sys value of type *tar.Header is safe as it is system-independent.
// The tar.FileInfoHeader function copies the fields into the returned
// header without performing any OS lookups.
+4 -5
View File
@@ -36,11 +36,10 @@ func sysStat(fi os.FileInfo, hdr *tar.Header) error {
hdr.Uid = int(s.Uid)
hdr.Gid = int(s.Gid)
if s.Mode&unix.S_IFBLK != 0 || s.Mode&unix.S_IFCHR != 0 {
// #nosec G115 -- Rdev type varies by platform.
rdev := uint64(s.Rdev) //nolint:unconvert // Rdev type varies by platform.
hdr.Devmajor = int64(unix.Major(rdev))
hdr.Devminor = int64(unix.Minor(rdev))
if s.Mode&unix.S_IFBLK != 0 ||
s.Mode&unix.S_IFCHR != 0 {
hdr.Devmajor = int64(unix.Major(uint64(s.Rdev))) //nolint: unconvert
hdr.Devminor = int64(unix.Minor(uint64(s.Rdev))) //nolint: unconvert
}
return nil
-4
View File
@@ -22,10 +22,6 @@ func init() {
}
}
// boundTime returns t if it falls within the range supported by os.Chtimes.
// Times before the Unix epoch (minTime) or after the end of Unix time
// (maxTime) are replaced with minTime, as os.Chtimes has undefined behavior
// outside that range.
func boundTime(t time.Time) time.Time {
if t.Before(minTime) || t.After(maxTime) {
return minTime
+18 -43
View File
@@ -3,55 +3,18 @@
package archive
import (
"errors"
"os"
"path"
"path/filepath"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
)
// chtimes changes the access and modification time of a file at the given
// path relative to root.
//
// Callers must use boundTime to ensure timestamps are within the range
// supported by os.Chtimes.
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
return root.Chtimes(name, atime, mtime)
}
func lchtimes(root *os.Root, name string, atime, mtime time.Time) error {
dir, base := path.Split(filepath.ToSlash(name))
if base == "" {
return &os.PathError{Op: "lchtimes", Path: name, Err: syscall.EINVAL}
}
dir = strings.TrimSuffix(dir, "/")
if dir == "" {
dir = "."
}
parent, err := root.Open(dir)
if err != nil {
return err
}
defer parent.Close()
utimes := [2]unix.Timespec{
timeToTimespec(atime),
timeToTimespec(mtime),
}
// #nosec G115 -- ignore integer overflow conversion for parent.Fd
if err := unix.UtimesNanoAt(int(parent.Fd()), base, utimes[:], unix.AT_SYMLINK_NOFOLLOW); err != nil {
if errors.Is(err, unix.ENOSYS) {
return nil
}
return &os.PathError{Op: "lchtimes", Path: name, Err: err}
}
return nil
// chtimes changes the access time and modified time of a file at the given path.
// If the modified time is prior to the Unix Epoch (unixMinTime), or after the
// end of Unix Time (unixEpochTime), os.Chtimes has undefined behavior. In this
// case, Chtimes defaults to Unix Epoch, just in case.
func chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(name, atime, mtime)
}
func timeToTimespec(time time.Time) unix.Timespec {
@@ -64,3 +27,15 @@ func timeToTimespec(time time.Time) unix.Timespec {
}
return unix.NsecToTimespec(time.UnixNano())
}
func lchtimes(name string, atime time.Time, mtime time.Time) error {
utimes := [2]unix.Timespec{
timeToTimespec(atime),
timeToTimespec(mtime),
}
err := unix.UtimesNanoAt(unix.AT_FDCWD, name, utimes[0:], unix.AT_SYMLINK_NOFOLLOW)
if err != nil && err != unix.ENOSYS {
return err
}
return err
}
+16 -95
View File
@@ -1,111 +1,32 @@
package archive
import (
"errors"
"os"
"path/filepath"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
// chtimes changes the access and modification time of a file at the given
// path relative to root.
//
// Symlink entries are handled separately through lchtimes. The final path
// component is expected not to be a reparse point; if one is encountered,
// chtimes returns an error.
//
// Callers must use boundTime to ensure timestamps are within the range
// supported by os.Chtimes.
func chtimes(root *os.Root, name string, atime, mtime time.Time) error {
parent, err := root.OpenFile(filepath.Dir(name), os.O_RDONLY, 0)
func chtimes(name string, atime time.Time, mtime time.Time) error {
if err := os.Chtimes(name, atime, mtime); err != nil {
return err
}
pathp, err := windows.UTF16PtrFromString(name)
if err != nil {
return err
}
defer parent.Close()
// Symlink entries are handled by lchtimes. The destination for all
// chtimes callers is therefore expected not to be a reparse point.
//
// Do not follow the final component: if it was concurrently replaced
// with a reparse point, fail instead of updating its target.
return chtimesAt(parent, filepath.Base(name), atime, mtime, true)
h, err := windows.CreateFile(pathp,
windows.FILE_WRITE_ATTRIBUTES, windows.FILE_SHARE_WRITE, nil,
windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
return err
}
defer windows.Close(h)
c := windows.NsecToFiletime(mtime.UnixNano())
return windows.SetFileTime(h, &c, nil, nil)
}
func lchtimes(root *os.Root, name string, atime time.Time, mtime time.Time) error {
func lchtimes(name string, atime time.Time, mtime time.Time) error {
return nil
}
func chtimesAt(parent *os.File, name string, atime, mtime time.Time, noFollow bool) error {
h, err := openForWriteAttributesAt(windows.Handle(parent.Fd()), name, noFollow)
if err != nil {
if noFollow && errors.Is(err, windows.STATUS_REPARSE_POINT_ENCOUNTERED) {
// Encountering a reparse point when noFollow is requested is unexpected.
// Treat it as a potential breakout to fail extraction safely.
return breakoutError(err)
}
return err
}
defer func() { _ = windows.Close(h) }()
var (
creationTime = windows.NsecToFiletime(mtime.UnixNano())
accessTime = windows.NsecToFiletime(atime.UnixNano())
modificationTime = windows.NsecToFiletime(mtime.UnixNano())
)
return windows.SetFileTime(h, &creationTime, &accessTime, &modificationTime)
}
// openForWriteAttributesAt opens name relative to parent with permission to
// modify its file attributes. If noFollow is true, it does not follow reparse
// points.
//
// This implementation is based on Go's internal Windows Openat support:
//
// https://github.com/golang/go/blob/go1.26.0/src/internal/syscall/windows/at_windows.go
//
// It is used by os.Root's Windows implementation for root-relative filesystem
// operations:
//
// https://github.com/golang/go/blob/go1.26.0/src/os/root_windows.go
//
// Keep this implementation aligned with the upstream code until an equivalent
// operation is available from golang.org/x/sys/windows.
func openForWriteAttributesAt(parent windows.Handle, name string, noFollow bool) (windows.Handle, error) {
name16, err := windows.UTF16FromString(name)
if err != nil {
return windows.InvalidHandle, err
}
attrs := uint32(windows.OBJ_CASE_INSENSITIVE)
if noFollow {
attrs |= windows.OBJ_DONT_REPARSE
}
var handle windows.Handle
err = windows.NtCreateFile(
&handle,
windows.SYNCHRONIZE|windows.FILE_WRITE_ATTRIBUTES,
&windows.OBJECT_ATTRIBUTES{
Length: uint32(unsafe.Sizeof(windows.OBJECT_ATTRIBUTES{})),
RootDirectory: parent,
ObjectName: &windows.NTUnicodeString{
Length: uint16((len(name16) - 1) * 2), // #nosec G115 -- Length is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
MaximumLength: uint16(len(name16) * 2), // #nosec G115 -- MaximumLength is USHORT by definition. A Windows path component cannot exceed uint16 bytes.
Buffer: &name16[0],
},
Attributes: attrs,
},
&windows.IO_STATUS_BLOCK{},
nil,
windows.FILE_ATTRIBUTE_NORMAL,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
windows.FILE_OPEN,
windows.FILE_OPEN_FOR_BACKUP_INTENT|windows.FILE_SYNCHRONOUS_IO_NONALERT,
0, // EA buffer
0, // EA length
)
return handle, err
}
+10 -10
View File
@@ -13,26 +13,26 @@ import (
// lgetxattr retrieves the value of the extended attribute identified by attr
// and associated with the given path in the file system.
// It returns a nil slice and nil error if the xattr is not set.
func lgetxattr(filePath string, attr string) ([]byte, error) {
func lgetxattr(path string, attr string) ([]byte, error) {
// Start with a 128 length byte array
dest := make([]byte, 128)
sz, err := unix.Lgetxattr(filePath, attr, dest)
sz, err := unix.Lgetxattr(path, attr, dest)
for errors.Is(err, unix.ERANGE) {
// Buffer too small, use zero-sized buffer to get the actual size
sz, err = unix.Lgetxattr(filePath, attr, []byte{})
sz, err = unix.Lgetxattr(path, attr, []byte{})
if err != nil {
return nil, wrapPathError("lgetxattr", filePath, attr, err)
return nil, wrapPathError("lgetxattr", path, attr, err)
}
dest = make([]byte, sz)
sz, err = unix.Lgetxattr(filePath, attr, dest)
sz, err = unix.Lgetxattr(path, attr, dest)
}
if err != nil {
if errors.Is(err, noattr) {
return nil, nil
}
return nil, wrapPathError("lgetxattr", filePath, attr, err)
return nil, wrapPathError("lgetxattr", path, attr, err)
}
return dest[:sz], nil
@@ -40,13 +40,13 @@ func lgetxattr(filePath string, attr string) ([]byte, error) {
// lsetxattr sets the value of the extended attribute identified by attr
// and associated with the given path in the file system.
func lsetxattr(filePath string, attr string, data []byte, flags int) error {
return wrapPathError("lsetxattr", filePath, attr, unix.Lsetxattr(filePath, attr, data, flags))
func lsetxattr(path string, attr string, data []byte, flags int) error {
return wrapPathError("lsetxattr", path, attr, unix.Lsetxattr(path, attr, data, flags))
}
func wrapPathError(op, filePath, attr string, err error) error {
func wrapPathError(op, path, attr string, err error) error {
if err == nil {
return nil
}
return &fs.PathError{Op: op, Path: filePath, Err: fmt.Errorf("xattr %q: %w", attr, err)}
return &fs.PathError{Op: op, Path: path, Err: fmt.Errorf("xattr %q: %w", attr, err)}
}
+1
View File
@@ -2,6 +2,7 @@
[![PkgGoDev](https://pkg.go.dev/badge/github.com/moby/moby/client)](https://pkg.go.dev/github.com/moby/moby/client)
![GitHub License](https://img.shields.io/github/license/moby/moby)
[![Go Report Card](https://goreportcard.com/badge/github.com/moby/moby/client)](https://goreportcard.com/report/github.com/moby/moby/client)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/moby/moby/badge)](https://scorecard.dev/viewer/?uri=github.com/moby/moby)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10989/badge)](https://www.bestpractices.dev/projects/10989)
+9 -10
View File
@@ -31,9 +31,8 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
if err != nil {
return ContainerCommitResult{}, err
}
query := url.Values{}
query.Set("container", containerID)
var repository, tag string
if options.Reference != "" {
ref, err := reference.ParseNormalizedNamed(options.Reference)
if err != nil {
@@ -45,18 +44,18 @@ func (cli *Client) ContainerCommit(ctx context.Context, containerID string, opti
}
ref = reference.TagNameOnly(ref)
query.Set("repo", ref.Name())
if tagged, ok := ref.(reference.Tagged); ok {
query.Set("tag", tagged.Tag())
tag = tagged.Tag()
}
repository = ref.Name()
}
if options.Comment != "" {
query.Set("comment", options.Comment)
}
if options.Author != "" {
query.Set("author", options.Author)
}
query := url.Values{}
query.Set("container", containerID)
query.Set("repo", repository)
query.Set("tag", tag)
query.Set("comment", options.Comment)
query.Set("author", options.Author)
for _, change := range options.Changes {
query.Add("changes", change)
}
+1 -2
View File
@@ -16,8 +16,7 @@ type ContainerRestartOptions struct {
// Timeout (optional) is the timeout (in seconds) to wait for the container
// to stop gracefully before forcibly terminating it with SIGKILL.
//
// - Use nil to use the container's configured timeout, or the engine default
// if the container has no configured timeout.
// - Use nil to use the default timeout (10 seconds).
// - Use '-1' to wait indefinitely.
// - Use '0' to not wait for the container to exit gracefully, and
// immediately proceeds to forcibly terminating the container.
+1 -2
View File
@@ -16,8 +16,7 @@ type ContainerStopOptions struct {
// Timeout (optional) is the timeout (in seconds) to wait for the container
// to stop gracefully before forcibly terminating it with SIGKILL.
//
// - Use nil to use the container's configured timeout, or the engine default
// if the container has no configured timeout.
// - Use nil to use the default timeout (10 seconds).
// - Use '-1' to wait indefinitely.
// - Use '0' to not wait for the container to exit gracefully, and
// immediately proceeds to forcibly terminating the container.

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