mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -05:00
Compare commits
@@ -5,7 +5,14 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "area/testing"
|
||||
- "area/ci"
|
||||
- "status/2-code-review"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
codeql-actions:
|
||||
patterns:
|
||||
- "github/codeql-action/*"
|
||||
docker-actions:
|
||||
patterns:
|
||||
- "docker/*"
|
||||
|
||||
+39
-48
@@ -35,7 +35,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -63,10 +65,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@@ -78,63 +80,50 @@ jobs:
|
||||
working-directory: ./build
|
||||
run: |
|
||||
mkdir /tmp/out
|
||||
platform=${{ matrix.platform }}
|
||||
platformPair=${platform//\//-}
|
||||
platformPair=${PLATFORM//\//-}
|
||||
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
|
||||
if [ -z "${{ matrix.use_glibc }}" ]; then
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
|
||||
fi
|
||||
env:
|
||||
PLATFORM: ${{ matrix.platform }}
|
||||
-
|
||||
name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: /tmp/out/*
|
||||
if-no-files-found: error
|
||||
|
||||
bin-image:
|
||||
runs-on: ubuntu-24.04
|
||||
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
|
||||
steps:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
|
||||
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@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.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
|
||||
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 }}
|
||||
|
||||
prepare-plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -143,7 +132,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -165,10 +156,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -46,9 +46,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 2
|
||||
persist-credentials: false
|
||||
# CodeQL 2.16.4's auto-build added support for multi-module repositories,
|
||||
# and is trying to be smart by searching for modules in every directory,
|
||||
# including vendor directories. If no module is found, it's creating one
|
||||
@@ -61,20 +62,20 @@ jobs:
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.3"
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -44,7 +44,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
@@ -63,7 +65,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
@@ -74,7 +76,7 @@ jobs:
|
||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
name: PR Review - Trigger
|
||||
on:
|
||||
pull_request:
|
||||
types: [ready_for_review, opened, review_requested]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
|
||||
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
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
COMMENT_JSON: ${{ toJSON(github.event.comment) }}
|
||||
run: |
|
||||
mkdir -p context
|
||||
printf '%s' "${{ github.event_name }}" > context/event_name.txt
|
||||
printf '%s' "$PR_NUMBER" > context/pr_number.txt
|
||||
printf '%s' "$PR_HEAD_SHA" > context/pr_head_sha.txt
|
||||
if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
|
||||
printf '%s' "$COMMENT_JSON" > context/comment.json
|
||||
fi
|
||||
|
||||
- name: Upload context
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: pr-review-context
|
||||
path: context/
|
||||
retention-days: 1
|
||||
@@ -0,0 +1,23 @@
|
||||
name: PR Review
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
workflow_run:
|
||||
workflows: ["PR Review - Trigger"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
review:
|
||||
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@baf90543d81f5de59751dfd10e6cf45e21a5a982 # v2.0.3
|
||||
permissions:
|
||||
contents: read # Read repository files and PR diffs
|
||||
pull-requests: write # Post review comments
|
||||
issues: write # Create security incident issues if secrets detected
|
||||
checks: write # (Optional) Show review progress as a check run
|
||||
id-token: write # Required for OIDC authentication to AWS Secrets Manager
|
||||
actions: read # Download artifacts from trigger workflow
|
||||
with:
|
||||
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
|
||||
@@ -0,0 +1,157 @@
|
||||
name: Sync Docker release branch
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.release_branch }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
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
|
||||
type: string
|
||||
dry_run:
|
||||
description: Merge but don't push
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
sync-release-branch:
|
||||
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 }}
|
||||
temporary_branch: ${{ steps.sync.outputs.temporary_branch }}
|
||||
temporary_sha: ${{ steps.sync.outputs.temporary_sha }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Configure git author
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Sync release branch to tag range
|
||||
id: sync
|
||||
env:
|
||||
DRY_RUN: ${{ inputs.dry_run }}
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
RUN_ATTEMPT: ${{ github.run_attempt }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
base_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
|
||||
temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT"
|
||||
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" \
|
||||
"$TAG" \
|
||||
> "$tags_file"
|
||||
|
||||
echo >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY"
|
||||
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"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes to push"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
git push origin "HEAD:refs/heads/$temporary_branch"
|
||||
echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
push-release-branch:
|
||||
needs: sync-release-branch
|
||||
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
environment: docker-releases
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Push release branch
|
||||
env:
|
||||
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
|
||||
RELEASE_BRANCH: ${{ inputs.release_branch }}
|
||||
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
|
||||
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
|
||||
run: |
|
||||
git fetch origin "$RELEASE_BRANCH"
|
||||
current_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
|
||||
if [[ "$current_sha" != "$BASE_SHA" ]]; then
|
||||
echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch origin "$TEMPORARY_BRANCH"
|
||||
current_temporary_sha=$(git rev-parse FETCH_HEAD)
|
||||
if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then
|
||||
echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git push origin "FETCH_HEAD:$RELEASE_BRANCH"
|
||||
|
||||
- name: Delete temporary branch
|
||||
env:
|
||||
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
|
||||
run: git push origin --delete "$TEMPORARY_BRANCH"
|
||||
@@ -30,15 +30,15 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -60,14 +60,15 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.3"
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
@@ -81,7 +82,7 @@ jobs:
|
||||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@a66e1c87e2eca0503c343edf1d208c716d54b8a8 # v7.1.0
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -48,7 +48,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Generate
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@@ -74,7 +76,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@@ -89,14 +93,15 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
path: src/github.com/docker/cli
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.3"
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
name: zizmor
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'master'
|
||||
- '[0-9]+.[0-9]+'
|
||||
- '[0-9]+.x'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run:
|
||||
uses: crazy-max/.github/.github/workflows/zizmor.yml@46267a6e61cd56aac2fc79943df180152f4c89d6 # v1.10.1
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
with:
|
||||
min-severity: medium
|
||||
min-confidence: medium
|
||||
persona: pedantic
|
||||
+1
-1
@@ -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.3"
|
||||
go: "1.26.7"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
|
||||
Alexander Morozov <lk4d4math@gmail.com>
|
||||
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
|
||||
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
|
||||
Alexandre Levavasseur <alexandre+oss@13x.fr>
|
||||
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
||||
Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
|
||||
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
|
||||
@@ -354,9 +355,10 @@ Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
|
||||
Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
Luo Jiyin <luojiyin@hotmail.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
|
||||
Lukas Michael <lukas.23022005@gmail.com>
|
||||
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
|
||||
Luo Jiyin <luojiyin@hotmail.com>
|
||||
Lyn <energylyn@zju.edu.cn>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com>
|
||||
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
|
||||
@@ -403,6 +405,7 @@ Michael Huettermann <michael@huettermann.net>
|
||||
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
|
||||
Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Mickael Emirkanian <mickael.emirkanian@docker.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
|
||||
Miguel Angel Fernández <elmendalerenda@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
|
||||
@@ -572,6 +575,8 @@ Ulysses Souza <ulysses.souza@docker.com>
|
||||
Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com>
|
||||
Umesh Yadav <umesh4257@gmail.com>
|
||||
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
|
||||
Varun Hotani <varunhotani@gmail.com>
|
||||
Vibhu Anan <vibhuanand@outlook.com>
|
||||
Victor Lyuboslavsky <victor@victoreda.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
|
||||
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
|
||||
|
||||
@@ -43,6 +43,7 @@ Alexander Larsson <alexl@redhat.com>
|
||||
Alexander Morozov <lk4d4math@gmail.com>
|
||||
Alexander Ryabov <i@sepa.spb.ru>
|
||||
Alexandre González <agonzalezro@gmail.com>
|
||||
Alexandre Levavasseur <alexandre+oss@13x.fr>
|
||||
Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
|
||||
Alexey Igrychev <alexey.igrychev@flant.com>
|
||||
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
|
||||
@@ -161,6 +162,7 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
|
||||
Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
|
||||
Chen Mingjie <chenmingjie0828@163.com>
|
||||
Chen Qiu <cheney-90@hotmail.com>
|
||||
Ching Wei Kang <164879897+WilliamK112@users.noreply.github.com>
|
||||
Chris Chinchilla <chris@chrischinchilla.com>
|
||||
Chris Couzens <ccouzens@gmail.com>
|
||||
Chris Gavin <chris@chrisgavin.me>
|
||||
@@ -366,7 +368,7 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
|
||||
huqun <huqun@zju.edu.cn>
|
||||
Huu Nguyen <huu@prismskylabs.com>
|
||||
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
|
||||
Iain MacDonald <ijmacd@gmail.com>
|
||||
Iain MacDonald <IJMacD@gmail.com>
|
||||
Iain Samuel McLean Elder <iain@isme.es>
|
||||
Ian Campbell <ian.campbell@docker.com>
|
||||
Ian Philpot <ian.philpot@microsoft.com>
|
||||
@@ -552,6 +554,7 @@ Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
|
||||
Luis Henrique Mulinari <luis.mulinari@gmail.com>
|
||||
Luka Hartwig <mail@lukahartwig.de>
|
||||
Lukas Heeren <lukas-heeren@hotmail.com>
|
||||
Lukas Michael <lukas.23022005@gmail.com>
|
||||
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
|
||||
Luo Jiyin <luojiyin@hotmail.com>
|
||||
Lydell Manganti <LydellManganti@users.noreply.github.com>
|
||||
@@ -562,6 +565,7 @@ Maciej Kalisz <maciej.d.kalisz@gmail.com>
|
||||
Madhav Puri <madhav.puri@gmail.com>
|
||||
Madhu Venugopal <madhu@socketplane.io>
|
||||
Madhur Batra <madhurbatra097@gmail.com>
|
||||
Mahesh Thakur <maheshthakur9152@gmail.com>
|
||||
Malte Janduda <mail@janduda.net>
|
||||
Manjunath A Kumatagi <mkumatag@in.ibm.com>
|
||||
Mansi Nahar <mmn4185@rit.edu>
|
||||
@@ -589,10 +593,12 @@ Mathieu Rollet <matletix@gmail.com>
|
||||
Matt Gucci <matt9ucci@gmail.com>
|
||||
Matt Robenolt <matt@ydekproductions.com>
|
||||
Matteo Orefice <matteo.orefice@bites4bits.software>
|
||||
Matteo Panzeri <matteo1782@gmail.com>
|
||||
Matthew Heon <mheon@redhat.com>
|
||||
Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
|
||||
Matthieu MOREL <matthieu.morel35@gmail.com>
|
||||
Mauro Porras P <mauroporrasp@gmail.com>
|
||||
Max Morozov <gtmax.yo@gmail.com>
|
||||
Max Shytikov <mshytikov@gmail.com>
|
||||
Max-Julian Pogner <max-julian@pogner.at>
|
||||
Maxime Petazzoni <max@signalfuse.com>
|
||||
@@ -617,6 +623,7 @@ Michael West <mwest@mdsol.com>
|
||||
Michael Zampani <michael.zampani@docker.com>
|
||||
Michal Minář <miminar@redhat.com>
|
||||
Michał Czeraszkiewicz <czerasz@gmail.com>
|
||||
Mickael Emirkanian <mickael.emirkanian@docker.com>
|
||||
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
|
||||
Mihai Borobocea <MihaiBorob@gmail.com>
|
||||
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
|
||||
@@ -638,6 +645,7 @@ Mohammad Banikazemi <mb@us.ibm.com>
|
||||
Mohammad Hossein <mhm98035@gmail.com>
|
||||
Mohammed Aaqib Ansari <maaquib@gmail.com>
|
||||
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
|
||||
Mohammed Thaha <mohammedthahacse@gmail.com>
|
||||
Mohini Anne Dsouza <mohini3917@gmail.com>
|
||||
Moorthy RS <rsmoorthy@gmail.com>
|
||||
Morgan Bauer <mbauer@us.ibm.com>
|
||||
@@ -689,6 +697,7 @@ Olli Janatuinen <olli.janatuinen@gmail.com>
|
||||
Oscar Wieman <oscrx@icloud.com>
|
||||
Otto Kekäläinen <otto@seravo.fi>
|
||||
Ovidio Mallo <ovidio.mallo@gmail.com>
|
||||
Park Jaeon <me@finalchild.dev>
|
||||
Pascal Borreli <pascal@borreli.com>
|
||||
Patrick Böänziger <patrick.baenziger@bsi-software.com>
|
||||
Patrick Daigle <114765035+pdaig@users.noreply.github.com>
|
||||
@@ -715,6 +724,7 @@ Peter Jaffe <pjaffe@nevo.com>
|
||||
Peter Kehl <peter.kehl@gmail.com>
|
||||
Peter Nagy <xificurC@gmail.com>
|
||||
Peter Salvatore <peter@psftw.com>
|
||||
Peter Valdemar Mørch <peter@morch.com>
|
||||
Peter Waller <p@pwaller.net>
|
||||
Phil Estes <estesp@gmail.com>
|
||||
Philip Alexander Etling <paetling@gmail.com>
|
||||
@@ -739,6 +749,7 @@ Ray Tsang <rayt@google.com>
|
||||
Reficul <xuzhenglun@gmail.com>
|
||||
Remy Suen <remy.suen@gmail.com>
|
||||
Renaud Gaubert <rgaubert@nvidia.com>
|
||||
René Hermenau <rene-hermenau@users.noreply.github.com>
|
||||
Ricardo N Feliciano <FelicianoTech@gmail.com>
|
||||
Rich Moyse <rich@moyse.us>
|
||||
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
|
||||
@@ -789,6 +800,7 @@ Scott Collier <emailscottcollier@gmail.com>
|
||||
Sean Christopherson <sean.j.christopherson@intel.com>
|
||||
Sean Rodman <srodman7689@gmail.com>
|
||||
Sebastiaan van Stijn <github@gone.nl>
|
||||
Seiya Miyata <odradek38@gmail.com>
|
||||
Sergey Tryuber <Sergeant007@users.noreply.github.com>
|
||||
Serhat Gülçiçek <serhat25@gmail.com>
|
||||
Sevki Hasirci <s@sevki.org>
|
||||
@@ -889,8 +901,10 @@ Umesh Yadav <umesh4257@gmail.com>
|
||||
Vaclav Struhar <struharv@gmail.com>
|
||||
Valentin Lorentz <progval+git@progval.net>
|
||||
Vardan Pogosian <vardan.pogosyan@gmail.com>
|
||||
Varun Hotani <varunhotani@gmail.com>
|
||||
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
|
||||
Veres Lajos <vlajos@gmail.com>
|
||||
Vibhu Anan <vibhuanand@outlook.com>
|
||||
Victor Vieux <victor.vieux@docker.com>
|
||||
Victoria Bialas <victoria.bialas@docker.com>
|
||||
Viktor Stanchev <me@viktorstanchev.com>
|
||||
|
||||
+1
-1
@@ -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 your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
Use a known identity (sorry, no 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
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.3
|
||||
ARG GO_VERSION=1.26.7
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[](https://pkg.go.dev/github.com/docker/cli)
|
||||
[](https://github.com/docker/cli/actions?query=workflow%3Abuild)
|
||||
[](https://github.com/docker/cli/actions?query=workflow%3Atest)
|
||||
[](https://goreportcard.com/report/github.com/docker/cli)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/docker/cli)
|
||||
[](https://codecov.io/gh/docker/cli)
|
||||
|
||||
|
||||
+33
-15
@@ -208,52 +208,70 @@ 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)
|
||||
|
||||
receiveReqCh := make(chan bool)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
defer cancel()
|
||||
// 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{})
|
||||
|
||||
// Simulate a server that hangs on connections.
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
case receiveReqCh <- true: // Blocks until someone receives on the channel.
|
||||
case receivedReqCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
<-releaseHandlerCh
|
||||
}))
|
||||
ts.Listener = l
|
||||
ts.Start()
|
||||
defer ts.Close()
|
||||
t.Cleanup(func() {
|
||||
close(releaseHandlerCh)
|
||||
ts.Close()
|
||||
})
|
||||
|
||||
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
|
||||
configFile := &configfile.ConfigFile{}
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
|
||||
initializedCh := make(chan bool)
|
||||
initErrCh := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
|
||||
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.Check(t, err)
|
||||
cli.CurrentVersion()
|
||||
close(initializedCh)
|
||||
initErrCh <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
case err := <-initErrCh:
|
||||
assert.Check(t, err)
|
||||
case <-time.After(waitTimeout):
|
||||
t.Fatal("timeout waiting for initialization to complete")
|
||||
case <-initializedCh:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-timeoutCtx.Done():
|
||||
case <-receivedReqCh:
|
||||
case <-time.After(waitTimeout):
|
||||
t.Fatal("server never received an init request")
|
||||
case <-receiveReqCh:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,28 @@ 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.
|
||||
|
||||
@@ -196,6 +196,18 @@ 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -88,7 +88,6 @@ 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)
|
||||
|
||||
@@ -52,6 +52,7 @@ 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"})
|
||||
})
|
||||
|
||||
@@ -458,7 +458,7 @@ func comparePorts(i, j container.PortSummary) bool {
|
||||
}
|
||||
|
||||
if i.IP != j.IP {
|
||||
return i.IP.String() < j.IP.String()
|
||||
return i.IP.Less(j.IP)
|
||||
}
|
||||
|
||||
if i.PublicPort != j.PublicPort {
|
||||
|
||||
@@ -946,6 +946,24 @@ 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 {
|
||||
|
||||
@@ -123,6 +123,7 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
return err
|
||||
}
|
||||
|
||||
var notes []string
|
||||
defer func() {
|
||||
_ = responseBody.Close()
|
||||
for _, note := range notes {
|
||||
@@ -131,18 +132,16 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
}()
|
||||
|
||||
if opts.quiet {
|
||||
err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux()))
|
||||
err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux(¬es, out)))
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), ref.String())
|
||||
}
|
||||
return err
|
||||
}
|
||||
return jsonstream.Display(ctx, responseBody, dockerCli.Out(), jsonstream.WithAuxCallback(handleAux()))
|
||||
return jsonstream.Display(ctx, responseBody, dockerCli.Out(), jsonstream.WithAuxCallback(handleAux(¬es, out)))
|
||||
}
|
||||
|
||||
var notes []string
|
||||
|
||||
func handleAux() func(jm jsonstream.JSONMessage) {
|
||||
func handleAux(notes *[]string, out tui.Output) func(jm jsonstream.JSONMessage) {
|
||||
return func(jm jsonstream.JSONMessage) {
|
||||
b := []byte(*jm.Aux)
|
||||
|
||||
@@ -150,10 +149,10 @@ func handleAux() func(jm jsonstream.JSONMessage) {
|
||||
err := json.Unmarshal(b, &stripped)
|
||||
if err == nil && stripped.ManifestPushedInsteadOfIndex {
|
||||
note := fmt.Sprintf("Not all multiplatform-content is present and only the available single-platform image was pushed\n%s -> %s",
|
||||
aec.RedF.Apply(stripped.OriginalIndex.Digest.String()),
|
||||
aec.GreenF.Apply(stripped.SelectedManifest.Digest.String()),
|
||||
out.Color(aec.RedF).Apply(stripped.OriginalIndex.Digest.String()),
|
||||
out.Color(aec.GreenF).Apply(stripped.SelectedManifest.Digest.String()),
|
||||
)
|
||||
notes = append(notes, note)
|
||||
*notes = append(*notes, note)
|
||||
}
|
||||
|
||||
var missing auxprogress.ContentMissing
|
||||
@@ -166,7 +165,7 @@ func handleAux() func(jm jsonstream.JSONMessage) {
|
||||
Make sure you have all the referenced content and try again.
|
||||
|
||||
You can also push only a single platform specific manifest directly by specifying the platform you want to push with the --platform flag.`
|
||||
notes = append(notes, note)
|
||||
*notes = append(*notes, note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/auxprogress"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -86,3 +91,28 @@ func TestNewPushCommandSuccess(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPushRespectsNoColorForAuxNotes(t *testing.T) {
|
||||
t.Setenv("NO_COLOR", "1")
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
imagePushFunc: func(ref string, options client.ImagePushOptions) (client.ImagePushResponse, error) {
|
||||
aux, err := json.Marshal(auxprogress.ManifestPushedInsteadOfIndex{
|
||||
ManifestPushedInsteadOfIndex: true,
|
||||
OriginalIndex: ocispec.Descriptor{Digest: "sha256:1111111111111111111111111111111111111111111111111111111111111111"},
|
||||
SelectedManifest: ocispec.Descriptor{Digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
line := append([]byte(`{"aux":`), aux...)
|
||||
line = append(line, '}', '\n')
|
||||
return fakeStreamResult{ReadCloser: io.NopCloser(bytes.NewReader(line))}, nil
|
||||
},
|
||||
})
|
||||
cli.Out().SetIsTerminal(true)
|
||||
|
||||
err := runPush(t.Context(), cli, pushOptions{remote: "image:tag"})
|
||||
assert.NilError(t, err)
|
||||
|
||||
out := cli.OutBuffer().String()
|
||||
assert.Assert(t, strings.Contains(out, "sha256:1111111111111111111111111111111111111111111111111111111111111111 -> sha256:2222222222222222222222222222222222222222222222222222222222222222"))
|
||||
assert.Assert(t, !strings.Contains(out, "\x1b["), "output should not contain ANSI escape codes, output: %s", out)
|
||||
}
|
||||
|
||||
@@ -48,27 +48,6 @@ 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 {
|
||||
@@ -78,8 +57,7 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
|
||||
}
|
||||
cfg := dockerCLI.ConfigFile()
|
||||
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
|
||||
configKey := getAuthConfigKey(domainName)
|
||||
a, _ := cfg.GetAuthConfig(configKey)
|
||||
a, _ := cfg.GetAuthConfig(domainName)
|
||||
return registry.AuthConfig{
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
|
||||
@@ -2,12 +2,26 @@ 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.
|
||||
@@ -35,3 +49,23 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ 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
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -97,17 +97,22 @@ 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 Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
|
||||
func PromptUserForCredentials(ctx context.Context, cli Streams, 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
|
||||
//
|
||||
// TODO(thaJeztah): we need to confirm if this special handling is still needed, as we may not be doing this in other places.
|
||||
stdIn := cli.In()
|
||||
if runtime.GOOS == "windows" {
|
||||
cli.SetIn(streams.NewIn(os.Stdin))
|
||||
// TODO(thaJeztah); change to io.Reader and skip wrapping once prompt.DisableInputEcho no longer requires a streams.In
|
||||
stdIn = streams.NewIn(os.Stdin)
|
||||
}
|
||||
|
||||
argUser = strings.TrimSpace(argUser)
|
||||
@@ -132,7 +137,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
}
|
||||
|
||||
var err error
|
||||
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
|
||||
argUser, err = prompt.ReadInput(ctx, stdIn, cli.Out(), msg)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@@ -146,7 +151,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
|
||||
isEmpty := strings.TrimSpace(argPassword) == ""
|
||||
if isEmpty {
|
||||
restoreInput, err := prompt.DisableInputEcho(cli.In())
|
||||
restoreInput, err := prompt.DisableInputEcho(stdIn)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@@ -166,7 +171,7 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
|
||||
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
|
||||
}
|
||||
|
||||
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
argPassword, err = prompt.ReadInput(ctx, stdIn, cli.Out(), "Password: ")
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func newLoginCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVarP(&opts.user, "username", "u", "", "Username")
|
||||
flags.StringVarP(&opts.password, "password", "p", "", "Password or Personal Access Token (PAT)")
|
||||
flags.StringVarP(&opts.password, "password", "p", "", `Password or Personal Access Token (PAT), or "-" to read from stdin`)
|
||||
flags.BoolVar(&opts.passwordStdin, "password-stdin", false, "Take the Password or Personal Access Token (PAT) from stdin")
|
||||
|
||||
return cmd
|
||||
@@ -72,8 +72,8 @@ func newLoginCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
//
|
||||
// TODO(thaJeztah); combine with verifyLoginOptions, but this requires rewrites of many tests.
|
||||
func verifyLoginFlags(flags *pflag.FlagSet, opts loginOptions) error {
|
||||
if flags.Changed("password-stdin") {
|
||||
if flags.Changed("password") {
|
||||
if flags.Changed("password-stdin") || opts.password == "-" {
|
||||
if flags.Changed("password") && opts.password != "-" {
|
||||
return errors.New("conflicting options: cannot specify both --password and --password-stdin")
|
||||
}
|
||||
if !flags.Changed("username") {
|
||||
@@ -122,6 +122,11 @@ func readSecretFromStdin(r io.Reader) (string, error) {
|
||||
}
|
||||
|
||||
func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
|
||||
if opts.password == "-" {
|
||||
opts.password = ""
|
||||
opts.passwordStdin = true
|
||||
}
|
||||
|
||||
if opts.password != "" {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
|
||||
}
|
||||
|
||||
@@ -339,6 +339,57 @@ func TestRunLogin(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password dash reads password from stdin",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
stdIn: "my password\r\n",
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: "-",
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "my password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password dash empty stdin",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: "-",
|
||||
},
|
||||
expectedErr: `password is empty`,
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password dash and password stdin read password from stdin",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
stdIn: "my password\r\n",
|
||||
input: loginOptions{
|
||||
serverAddress: "reg1",
|
||||
user: "my-username",
|
||||
password: "-",
|
||||
passwordStdin: true,
|
||||
},
|
||||
expectedCredentials: map[string]configtypes.AuthConfig{
|
||||
"reg1": {
|
||||
Username: "my-username",
|
||||
Password: "my password",
|
||||
ServerAddress: "reg1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "password with leading and trailing spaces",
|
||||
priorCredentials: map[string]configtypes.AuthConfig{},
|
||||
@@ -397,7 +448,7 @@ func TestRunLogin(t *testing.T) {
|
||||
cfg := configfile.New(filepath.Join(tmpDir, "config.json"))
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
cli.SetConfigFile(cfg)
|
||||
if tc.input.passwordStdin {
|
||||
if tc.input.passwordStdin || tc.input.password == "-" || tc.stdIn != "" {
|
||||
if tc.expectedErr == "TEST_READ_ERR" {
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(iotest.ErrReader(errors.New(tc.expectedErr)))))
|
||||
} else {
|
||||
@@ -632,6 +683,21 @@ func TestLoginValidateFlags(t *testing.T) {
|
||||
args: []string{"--password-stdin", "--password", ""},
|
||||
expectedErr: `conflicting options: cannot specify both --password and --password-stdin`,
|
||||
},
|
||||
{
|
||||
name: "password stdin and password dash without stdin",
|
||||
args: []string{"--password-stdin", "--username", "my-username", "--password", "-"},
|
||||
expectedErr: `password is empty`,
|
||||
},
|
||||
{
|
||||
name: "password dash without username",
|
||||
args: []string{"--password", "-"},
|
||||
expectedErr: `the --password-stdin option requires --username to be set`,
|
||||
},
|
||||
{
|
||||
name: "short password dash without username",
|
||||
args: []string{"-p", "-"},
|
||||
expectedErr: `the --password-stdin option requires --username to be set`,
|
||||
},
|
||||
{
|
||||
name: "empty --password",
|
||||
args: []string{"--password", ""},
|
||||
@@ -658,3 +724,10 @@ func TestLoginValidateFlags(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginHelpDocumentsPasswordDash(t *testing.T) {
|
||||
cmd := newLoginCommand(test.NewFakeCli(&fakeClient{}))
|
||||
flag := cmd.Flags().Lookup("password")
|
||||
assert.Check(t, flag != nil)
|
||||
assert.Check(t, is.Contains(flag.Usage, `"-"`))
|
||||
}
|
||||
|
||||
@@ -2,12 +2,32 @@ 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.
|
||||
@@ -31,3 +51,74 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ 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,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -687,15 +688,13 @@ func (options *serviceOptions) makeEnv() ([]string, error) {
|
||||
}
|
||||
currentEnv := make([]string, 0, len(envVariables))
|
||||
for _, env := range envVariables { // need to process each var, in order
|
||||
k, _, _ := strings.Cut(env, "=")
|
||||
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:]...)
|
||||
}
|
||||
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+"=")
|
||||
})
|
||||
currentEnv = append(currentEnv, env)
|
||||
}
|
||||
|
||||
|
||||
@@ -373,3 +373,49 @@ 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ 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
|
||||
}
|
||||
|
||||
|
||||
@@ -1213,11 +1213,9 @@ func updateHosts(flags *pflag.FlagSet, hosts *[]string) error {
|
||||
if rm.IPAddr != "" && rm.IPAddr != ip {
|
||||
continue
|
||||
}
|
||||
for i, h := range hostNames {
|
||||
if h == rm.Host {
|
||||
hostNames = append(hostNames[:i], hostNames[i+1:]...)
|
||||
}
|
||||
}
|
||||
hostNames = slices.DeleteFunc(hostNames, func(h string) bool {
|
||||
return h == rm.Host
|
||||
})
|
||||
}
|
||||
if len(hostNames) > 0 {
|
||||
newHosts = append(newHosts, fmt.Sprintf("%s %s", ip, strings.Join(hostNames, " ")))
|
||||
|
||||
@@ -1727,3 +1727,18 @@ 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))
|
||||
}
|
||||
|
||||
@@ -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 the equivalent to [registry.ConvertToHostname] in the daemon.
|
||||
// It's based on [registry.ConvertToHostname] from Moby 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,7 +117,48 @@ 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
|
||||
}
|
||||
|
||||
@@ -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.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
|
||||
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
|
||||
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
@@ -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.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
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/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.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
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/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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
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/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
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=
|
||||
|
||||
+4
-3
@@ -8,6 +8,7 @@
|
||||
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 ?=
|
||||
@@ -62,7 +63,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=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--mount 'type=bind,src=$(DOCKER_SOCK),dst=/var/run/docker.sock' \
|
||||
$(DEV_DOCKER_IMAGE_NAME)
|
||||
|
||||
shell: dev ## alias for dev
|
||||
@@ -134,14 +135,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=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--mount 'type=bind,src=$(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=/var/run/docker.sock,dst=/var/run/docker.sock \
|
||||
--mount 'type=bind,src=$(DOCKER_SOCK),dst=/var/run/docker.sock' \
|
||||
$(E2E_IMAGE_NAME)
|
||||
|
||||
.PHONY: help
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.3
|
||||
ARG GO_VERSION=1.26.7
|
||||
|
||||
# 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,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.3
|
||||
ARG GO_VERSION=1.26.7
|
||||
|
||||
# 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,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG GO_VERSION=1.26.3
|
||||
ARG GO_VERSION=1.26.7
|
||||
|
||||
# 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
|
||||
|
||||
@@ -113,6 +113,7 @@ The following table provides an overview of the current status of deprecated fea
|
||||
| Deprecated | [`-h` shorthand for `--help`](#-h-shorthand-for---help) | v1.12 | v17.09 |
|
||||
| Removed | [`-e` and `--email` flags on `docker login`](#-e-and---email-flags-on-docker-login) | v1.11 | v17.06 |
|
||||
| Deprecated | [Separator (`:`) of `--security-opt` flag on `docker run`](#separator--of---security-opt-flag-on-docker-run) | v1.11 | v17.06 |
|
||||
| Deprecated | [Links on the default bridge network](#links-on-the-default-bridge-network) | v1.10 | - |
|
||||
| Deprecated | [Ambiguous event fields in API](#ambiguous-event-fields-in-api) | v1.10 | - |
|
||||
| Removed | [`-f` flag on `docker tag`](#-f-flag-on-docker-tag) | v1.10 | v1.12 |
|
||||
| Removed | [HostConfig at API container start](#hostconfig-at-api-container-start) | v1.10 | v1.12 |
|
||||
@@ -1203,6 +1204,19 @@ The `docker login` no longer automatically registers an account with the target
|
||||
|
||||
The flag `--security-opt` doesn't use the colon separator (`:`) anymore to divide keys and values, it uses the equal symbol (`=`) for consistency with other similar flags, like `--storage-opt`.
|
||||
|
||||
### Links on the default bridge network
|
||||
|
||||
**Deprecated in release: v1.10**
|
||||
**Target for removal in release: v30.0**
|
||||
|
||||
The `--link` option on `docker create` and `docker run`, when used with no
|
||||
`--network` specified, was deprecated in v1.10 and will be removed in a future
|
||||
release. Custom networks should be used instead. Docker 29.6 added a deprecation
|
||||
warning when this option is used for the default bridge network.
|
||||
|
||||
Note that the `--link` option is still supported when a non-default network
|
||||
is used.
|
||||
|
||||
### Ambiguous event fields in API
|
||||
|
||||
**Deprecated in release: [v1.10.0](https://github.com/docker/docker/releases/tag/v1.10.0)**
|
||||
|
||||
@@ -74,11 +74,14 @@ The sequence diagrams below depict an allow and deny authorization flow:
|
||||
Each request sent to the plugin includes the authenticated user, the HTTP
|
||||
headers, and the request/response body. Only the user name and the
|
||||
authentication method used are passed to the plugin. Most importantly, no user
|
||||
credentials or tokens are passed. Finally, not all request/response bodies
|
||||
are sent to the authorization plugin. Only request/response bodies where
|
||||
the `Content-Type` is `application/json` are sent to the authorization plugin;
|
||||
bodies of any other `Content-Type` are not visible to the plugin and cannot
|
||||
be used for enforcement, even though the daemon may still act on this data.
|
||||
credentials or tokens are passed.
|
||||
|
||||
> [!NOTE]
|
||||
> Authorization plugins enforce requests to the Docker daemon's HTTP API only. gRPC method
|
||||
> calls, whether dispatched natively or upgraded through `POST /grpc`, are not subject to authorization.
|
||||
> Furthermore, HTTP request/response bodies where the `Content-Type` is `application/json` are forwarded;
|
||||
> bodies of any other type are not visible to the plugin and cannot be used for enforcement,
|
||||
> even though the daemon acts on this data.
|
||||
|
||||
For commands that can potentially hijack the HTTP connection (`HTTP
|
||||
Upgrade`), such as `exec`, the authorization plugin is only called for the
|
||||
@@ -88,6 +91,22 @@ passed to the authorization plugins. For commands that return chunked HTTP
|
||||
response, such as `logs` and `events`, only the HTTP request is sent to the
|
||||
authorization plugins.
|
||||
|
||||
The Engine's authorization middleware fails closed: when a plugin returns an error or returns `Allow: false`,
|
||||
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
|
||||
@@ -104,15 +123,6 @@ 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
|
||||
@@ -236,7 +246,7 @@ Name | Type | Description
|
||||
User | string | The user identification
|
||||
Authentication method | string | The authentication method used
|
||||
Request method | enum | The HTTP method (GET/DELETE/POST)
|
||||
Request URI | string | The HTTP request URI including API version (e.g., v.1.17/containers/json)
|
||||
Request URI | string | The HTTP request URI including API version, as sent by the client (e.g., v.1.17/containers/json)
|
||||
Request headers | map[string]string | Request headers as key value pairs (without the authorization header)
|
||||
Request body | []byte | Raw request body
|
||||
|
||||
@@ -259,7 +269,7 @@ Name | Type | Description
|
||||
User | string | The user identification
|
||||
Authentication method | string | The authentication method used
|
||||
Request method | string | The HTTP method (GET/DELETE/POST)
|
||||
Request URI | string | The HTTP request URI including API version (e.g., v.1.17/containers/json)
|
||||
Request URI | string | The HTTP request URI including API version, as sent by the client (e.g., v.1.17/containers/json)
|
||||
Request headers | map[string]string | Request headers as key value pairs (without the authorization header)
|
||||
Request body | []byte | Raw request body
|
||||
Response status code | int | Status code from the Docker daemon
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -9,14 +9,14 @@ Fetch the logs of a container
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------|
|
||||
| `--details` | `bool` | | Show extra details provided to logs |
|
||||
| `-f`, `--follow` | `bool` | | Follow log output |
|
||||
| `--since` | `string` | | Show logs since timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) |
|
||||
| `-n`, `--tail` | `string` | `all` | Number of lines to show from the end of the logs |
|
||||
| `-t`, `--timestamps` | `bool` | | Show timestamps |
|
||||
| [`--until`](#until) | `string` | | Show logs before a timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) |
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------|
|
||||
| [`--details`](#details) | `bool` | | Show extra details provided to logs |
|
||||
| [`-f`](#follow), [`--follow`](#follow) | `bool` | | Follow log output |
|
||||
| [`--since`](#since) | `string` | | Show logs since timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) |
|
||||
| [`-n`](#tail), [`--tail`](#tail) | `string` | `all` | Number of lines to show from the end of the logs |
|
||||
| [`-t`](#timestamps), [`--timestamps`](#timestamps) | `bool` | | Show timestamps |
|
||||
| [`--until`](#until) | `string` | | Show logs before a timestamp (e.g. `2013-01-02T13:23:37Z`) or relative (e.g. `42m` for 42 minutes) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
@@ -28,21 +28,34 @@ The `docker logs` command batch-retrieves logs present at the time of execution.
|
||||
For more information about selecting and configuring logging drivers, refer to
|
||||
[Configure logging drivers](https://docs.docker.com/engine/logging/configure/).
|
||||
|
||||
## Examples
|
||||
|
||||
### <a name="follow"></a> Stream log output (-f, --follow)
|
||||
|
||||
The `docker logs --follow` command will continue streaming the new output from
|
||||
the container's `STDOUT` and `STDERR`.
|
||||
|
||||
### <a name="tail"></a> Retrieve the last logs (-n, --tail)
|
||||
|
||||
Passing a negative number or a non-integer to `--tail` is invalid and the
|
||||
value is set to `all` in that case.
|
||||
|
||||
|
||||
### <a name="timestamps"></a> Retrieve logs with timestamps (-t, --timestamps)
|
||||
|
||||
The `docker logs --timestamps` command will add an [RFC3339Nano timestamp](https://pkg.go.dev/time#RFC3339Nano)
|
||||
, for example `2014-09-16T06:17:46.000000000Z`, to each
|
||||
log entry. To ensure that the timestamps are aligned the
|
||||
nano-second part of the timestamp will be padded with zero when necessary.
|
||||
|
||||
### <a name="details"></a> Retrieve logs with additional attributes (--details)
|
||||
|
||||
The `docker logs --details` command will add on extra attributes, such as
|
||||
environment variables and labels, provided to `--log-opt` when creating the
|
||||
container.
|
||||
|
||||
### <a name="since"></a> Retrieve logs generated since a specific point in time (--since)
|
||||
|
||||
The `--since` option shows only the container logs generated after
|
||||
a given date. You can specify the date as an RFC 3339 date, a UNIX
|
||||
timestamp, or a Go duration string (e.g. `1m30s`, `3h`). Besides RFC3339 date
|
||||
@@ -56,7 +69,6 @@ seconds (aka Unix epoch or Unix time), and the optional .nanoseconds field is a
|
||||
fraction of a second no more than nine digits long. You can combine the
|
||||
`--since` option with either or both of the `--follow` or `--tail` options.
|
||||
|
||||
## Examples
|
||||
|
||||
### <a name="until"></a> Retrieve logs until a specific point in time (--until)
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -431,7 +431,7 @@ a `docker` command. You can use the following protocols:
|
||||
|
||||
| Scheme | Description | Example |
|
||||
|----------------------------------------|---------------------------|----------------------------------|
|
||||
| `unix://[<path>]` | Unix socket (Linux only) | `unix:///var/run/docker.sock` |
|
||||
| `unix://[<path>]` | Unix socket | `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` |
|
||||
|
||||
@@ -6,11 +6,11 @@ Defaults to Docker Hub if no server is specified.
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------------------------------|:---------|:--------|:------------------------------------------------------------|
|
||||
| `-p`, `--password` | `string` | | Password or Personal Access Token (PAT) |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the Password or Personal Access Token (PAT) from stdin |
|
||||
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
|
||||
| Name | Type | Default | Description |
|
||||
|:---------------------------------------------|:---------|:--------|:-------------------------------------------------------------------|
|
||||
| `-p`, `--password` | `string` | | Password or Personal Access Token (PAT), or `-` to read from stdin |
|
||||
| [`--password-stdin`](#password-stdin) | `bool` | | Take the Password or Personal Access Token (PAT) from stdin |
|
||||
| [`-u`](#username), [`--username`](#username) | `string` | | Username |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
@@ -244,6 +244,13 @@ The following example reads a password from a file, and passes it to the
|
||||
$ cat ~/my_password.txt | docker login --username foo --password-stdin
|
||||
```
|
||||
|
||||
You can also pass `-` as the value for `--password` or `-p` to read the
|
||||
password from `STDIN`.
|
||||
|
||||
```console
|
||||
$ cat ~/my_password.txt | docker login --username foo --password -
|
||||
```
|
||||
|
||||
## Related commands
|
||||
|
||||
* [logout](logout.md)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# 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
|
||||
+28
-2
@@ -3,9 +3,35 @@ 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:
|
||||
image: 'docker:${ENGINE_VERSION:-29}-dind'
|
||||
build:
|
||||
context: ./testdata
|
||||
dockerfile: engine/Dockerfile
|
||||
args:
|
||||
ENGINE_VERSION: ${ENGINE_VERSION:-29}
|
||||
privileged: true
|
||||
command: ['--insecure-registry=registry:5000', '--experimental']
|
||||
command: ['--insecure-registry=registry:5000', '--insecure-registry=privateregistry:5001', '--experimental']
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=
|
||||
|
||||
@@ -209,7 +209,11 @@ func TestProcessTermination(t *testing.T) {
|
||||
|
||||
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGTERM))
|
||||
|
||||
icmd.WaitOnCmd(time.Second*10, result).Assert(t, icmd.Expected{
|
||||
// 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{
|
||||
ExitCode: 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
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")
|
||||
}
|
||||
@@ -23,6 +23,12 @@ 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
@@ -12,6 +12,9 @@ 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 /
|
||||
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
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
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
FROM registry:3
|
||||
COPY auth /auth
|
||||
COPY certs /certs
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
e2e:$2y$05$DxRBsGSy61vZsBgNVxwUh.UtZmlg3wZHMxYcHYAlupY7r1xbIiuoq
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
#!/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
|
||||
@@ -88,8 +88,18 @@ 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 = streams.NewIn(os.Stdin)
|
||||
in = os.Stdin
|
||||
}
|
||||
|
||||
result := make(chan bool)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
//go:build tools
|
||||
|
||||
package main
|
||||
|
||||
import _ "github.com/cpuguy83/go-md2man/v2"
|
||||
@@ -0,0 +1,84 @@
|
||||
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"))
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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"))
|
||||
}
|
||||
@@ -98,6 +98,16 @@ fi
|
||||
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ] && [ "$(go env GOOS)" = "linux" ]; then
|
||||
GO_LDFLAGS="$GO_LDFLAGS -linkmode external -extldflags -static"
|
||||
fi
|
||||
|
||||
# We only import google.golang.org/grpc as an indirect dependency, and
|
||||
# do not make gRPC connections.
|
||||
#
|
||||
# grpcnotrace avoids importing golang.org/x/net/trace, which in turn enables
|
||||
# dead code elimination, which can reduce binary size when tracing is not needed.
|
||||
#
|
||||
# see https://github.com/grpc/grpc-go/blob/v1.81.1/trace_notrace.go#L23-L25
|
||||
GO_BUILDTAGS="$GO_BUILDTAGS grpcnotrace"
|
||||
|
||||
if [ "$CGO_ENABLED" = "1" ] && [ "$GO_LINKMODE" = "static" ]; then
|
||||
# compiling statically with CGO enabled requires osusergo and netgo to be set.
|
||||
GO_BUILDTAGS="$GO_BUILDTAGS osusergo netgo"
|
||||
|
||||
@@ -7,15 +7,16 @@ set -eu
|
||||
if ! command -v "$GO_MD2MAN" > /dev/null; then
|
||||
(
|
||||
set -x
|
||||
go build -mod=vendor -modfile=vendor.mod -o ./build/tools/go-md2man ./vendor/github.com/cpuguy83/go-md2man/v2
|
||||
# note: this installs all tools defined in go.mod/vendor.mod
|
||||
GOBIN="$(pwd)/build/tools" go install -mod=vendor tool
|
||||
)
|
||||
GO_MD2MAN=$(realpath ./build/tools/go-md2man)
|
||||
GO_MD2MAN="$(pwd)/build/tools/go-md2man"
|
||||
fi
|
||||
|
||||
mkdir -p man/man1
|
||||
(
|
||||
set -x
|
||||
go run -mod=vendor -modfile=vendor.mod -tags manpages ./man/generate.go --source "./man/src" --target "./man/man1"
|
||||
go run -mod=vendor -tags manpages ./man/generate.go --source "./man/src" --target "./man/man1"
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
@@ -4,7 +4,7 @@ set -eu
|
||||
|
||||
(
|
||||
set -x
|
||||
go run -mod=vendor -modfile=vendor.mod -tags docsgen ./docs/generate/generate.go --formats md --source "./docs/reference/commandline" --target "./docs/reference/commandline"
|
||||
go run -mod=vendor -tags docsgen ./docs/generate/generate.go --formats md --source "./docs/reference/commandline" --target "./docs/reference/commandline"
|
||||
)
|
||||
|
||||
# remove generated help.md file
|
||||
|
||||
@@ -4,4 +4,4 @@ set -eu
|
||||
|
||||
mkdir -p docs/yaml
|
||||
set -x
|
||||
go run -mod=vendor -modfile=vendor.mod -tags docsgen ./docs/generate/generate.go --formats yaml --source "./docs/reference/commandline" --target "./docs/yaml"
|
||||
go run -mod=vendor -tags docsgen ./docs/generate/generate.go --formats yaml --source "./docs/reference/commandline" --target "./docs/yaml"
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Merge the given release tags.
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: $0 TAG..." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tags_to_sync=("$@")
|
||||
|
||||
for tag_to_sync in "${tags_to_sync[@]}"; do
|
||||
if git merge --no-edit --no-ff "$tag_to_sync"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify --quiet MERGE_HEAD > /dev/null || git diff --quiet --diff-filter=U; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git checkout "$tag_to_sync" -- '.'
|
||||
git add --all
|
||||
|
||||
# Can't use --no-edit with --continue
|
||||
EDITOR=true git merge --continue
|
||||
done
|
||||
|
||||
# Check that every tag is in the branch.
|
||||
# This catches cases where a merge did not actually incorporate one of the
|
||||
# requested release tags.
|
||||
for tag_to_sync in "${tags_to_sync[@]}"; do
|
||||
if ! git merge-base --is-ancestor "$tag_to_sync" HEAD; then
|
||||
echo "tag $tag_to_sync is not contained in $(git rev-parse --abbrev-ref HEAD)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -26,8 +26,55 @@ 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)"
|
||||
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "usage: $0 <RELEASE_BRANCH> <TAG>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_branch="$1"
|
||||
tag="$2"
|
||||
|
||||
if [[ "$tag" == *"-rc."* ]]; then
|
||||
echo "error: RC tags cannot be used as sync targets" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$tag" != v* ]]; then
|
||||
echo "error: Tag must start with 'v' (e.g. v29.6.0)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git rev-parse --verify --quiet "refs/tags/$tag" > /dev/null; then
|
||||
echo "error: Tag $tag does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Return early the requested tag is already merged into release branch.
|
||||
if git merge-base --is-ancestor "$tag" "$release_branch"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if git rev-parse --verify --quiet upstream/master > /dev/null 2>&1; then
|
||||
master="upstream/master"
|
||||
else
|
||||
master="origin/master"
|
||||
fi
|
||||
|
||||
if ! git merge-base --is-ancestor "$tag" "$master"; then
|
||||
echo "error: Tag $tag is not in $master" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get all docker release tags merged into master but not into release branch
|
||||
git tag --merged "$master" --no-merged "$release_branch" \
|
||||
| grep '^v' \
|
||||
| grep -v -- "-rc." \
|
||||
| sort -V \
|
||||
| awk -v tag="$tag" '{print} $0==tag{exit}'
|
||||
+2
-2
@@ -14,7 +14,7 @@ if [ -z "$TYP" ]; then
|
||||
fi
|
||||
|
||||
update() {
|
||||
(set -x ; go mod tidy -modfile=vendor.mod; go mod vendor -modfile=vendor.mod)
|
||||
(set -x ; go mod tidy; go mod vendor)
|
||||
}
|
||||
|
||||
validate() {
|
||||
@@ -31,7 +31,7 @@ outdated() {
|
||||
echo "go-mod-outdated not found. Install with 'go install github.com/psampaz/go-mod-outdated@v0.8.0'"
|
||||
exit 1
|
||||
fi
|
||||
(set -x ; go list -mod=vendor -mod=readonly -modfile=vendor.mod -u -m -json all | go-mod-outdated -update -direct)
|
||||
(set -x ; go list -mod=readonly -u -m -json all | go-mod-outdated -update -direct)
|
||||
}
|
||||
|
||||
case $TYP in
|
||||
|
||||
+23
-15
@@ -6,28 +6,36 @@
|
||||
# when the command is finished. This script should be dropped when this
|
||||
# repository is a proper Go module with a permanent go.mod.
|
||||
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPTDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOTDIR="$(cd "${SCRIPTDIR}/.." && pwd)"
|
||||
|
||||
if test -e "${ROOTDIR}/go.mod"; then
|
||||
{
|
||||
scriptname=$(basename "$0")
|
||||
cat >&2 <<- EOF
|
||||
$scriptname: WARN: go.mod exists in the repository root!
|
||||
$scriptname: WARN: Using your go.mod instead of our generated version -- this may misbehave!
|
||||
EOF
|
||||
} >&2
|
||||
else
|
||||
cleanup_paths=()
|
||||
|
||||
create_symlink() {
|
||||
local target="$1"
|
||||
local link="$2"
|
||||
|
||||
if [ -e "$link" ]; then
|
||||
# see https://superuser.com/a/196698
|
||||
if ! [ "$link" -ef "${ROOTDIR}/${target}" ]; then
|
||||
echo "$(basename "$0"): WARN: $link exists but is not the expected symlink!" >&2
|
||||
echo "$(basename "$0"): WARN: Using your version instead of our generated version -- this may misbehave!" >&2
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
set -x
|
||||
ln -s "$target" "$link"
|
||||
cleanup_paths+=( "$link" )
|
||||
}
|
||||
|
||||
tee "${ROOTDIR}/go.mod" >&2 <<- EOF
|
||||
module github.com/docker/cli
|
||||
create_symlink "vendor.mod" "${ROOTDIR}/go.mod"
|
||||
create_symlink "vendor.sum" "${ROOTDIR}/go.sum"
|
||||
|
||||
go 1.24.0
|
||||
EOF
|
||||
trap 'rm -f "${ROOTDIR}/go.mod"' EXIT
|
||||
if [ "${#cleanup_paths[@]}" -gt 0 ]; then
|
||||
trap 'rm -f "${cleanup_paths[@]}"' EXIT
|
||||
fi
|
||||
|
||||
GO111MODULE=on GOTOOLCHAIN=local "$@"
|
||||
|
||||
+36
-33
@@ -6,20 +6,22 @@ module github.com/docker/cli
|
||||
|
||||
go 1.25.0
|
||||
|
||||
tool golang.org/x/mod/modfile // for module compatibility check
|
||||
tool (
|
||||
github.com/cpuguy83/go-md2man/v2 // for scripts/docs/generate-man.sh
|
||||
golang.org/x/mod/modfile // for module compatibility check
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v1.0.0-rc.4
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli-docs-tool v0.11.0
|
||||
github.com/docker/distribution v2.8.3+incompatible
|
||||
github.com/docker/docker-credential-helpers v0.9.7
|
||||
github.com/docker/go-connections v0.7.0
|
||||
github.com/docker/docker-credential-helpers v0.9.8
|
||||
github.com/docker/go-connections v0.8.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
|
||||
@@ -28,15 +30,15 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/mattn/go-runewidth v0.0.23
|
||||
github.com/moby/go-archive v0.2.0
|
||||
github.com/moby/moby/api v1.54.2
|
||||
github.com/moby/moby/client v0.4.1
|
||||
github.com/mattn/go-runewidth v0.0.24
|
||||
github.com/moby/go-archive v0.3.3
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/moby/patternmatcher v0.6.1
|
||||
github.com/moby/swarmkit/v2 v2.1.2
|
||||
github.com/moby/sys/atomicwriter v0.1.0
|
||||
github.com/moby/sys/capability v0.4.0
|
||||
github.com/moby/sys/sequential v0.6.0
|
||||
github.com/moby/sys/sequential v0.7.0
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/sys/symlink v0.3.0
|
||||
github.com/moby/term v0.5.2
|
||||
@@ -44,24 +46,24 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/sirupsen/logrus v1.10.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
|
||||
go.opentelemetry.io/otel v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
|
||||
go.opentelemetry.io/otel/metric v1.43.0
|
||||
go.opentelemetry.io/otel/sdk v1.43.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||
go.opentelemetry.io/otel/trace v1.43.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.44.0
|
||||
golang.org/x/term v0.43.0
|
||||
golang.org/x/text v0.37.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
||||
go.opentelemetry.io/otel v1.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
|
||||
go.opentelemetry.io/otel/metric v1.44.0
|
||||
go.opentelemetry.io/otel/sdk v1.44.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0
|
||||
go.opentelemetry.io/otel/trace v1.44.0
|
||||
go.yaml.in/yaml/v3 v3.0.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
|
||||
gotest.tools/v3 v3.5.2
|
||||
tags.cncf.io/container-device-interface v1.1.0
|
||||
)
|
||||
@@ -74,7 +76,8 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/docker/go-events v0.0.0-20260608200158-dbf6103125a4 // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
@@ -82,11 +85,11 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.5 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/user v0.4.1 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
@@ -98,13 +101,13 @@ require (
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/raft/v3 v3.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/time v0.15.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
|
||||
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/protobuf v1.36.11 // indirect
|
||||
)
|
||||
|
||||
+70
-73
@@ -32,7 +32,6 @@ 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=
|
||||
@@ -40,12 +39,12 @@ github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQt
|
||||
github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA=
|
||||
github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
|
||||
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-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4=
|
||||
github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
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-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=
|
||||
github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -87,8 +86,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -96,27 +95,23 @@ 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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/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.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
|
||||
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.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.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||
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/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/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=
|
||||
@@ -125,14 +120,18 @@ 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/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
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=
|
||||
github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
|
||||
github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
|
||||
github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0=
|
||||
github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
@@ -153,7 +152,6 @@ 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=
|
||||
@@ -173,13 +171,11 @@ 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.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
|
||||
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
|
||||
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=
|
||||
@@ -189,8 +185,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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
|
||||
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
|
||||
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=
|
||||
@@ -206,53 +202,56 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0 h1:SUplec5dp06reu1zaXmOXdvqH398taqrDXqUl99jxSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0/go.mod h1:ho2g4N+ane+swq5I/VBkKWnRDY4kUINH3FuqyZqX/Ug=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
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.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.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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/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=
|
||||
@@ -261,14 +260,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.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/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/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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
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/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=
|
||||
@@ -281,18 +280,16 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
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=
|
||||
|
||||
+37
-13
@@ -1,21 +1,19 @@
|
||||
package sockets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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
|
||||
// inmemAddr is used to satisfy net.Addr for the in-memory socket.
|
||||
type inmemAddr string
|
||||
|
||||
// Network returns the addr string, satisfies net.Addr
|
||||
func (a dummyAddr) Network() string {
|
||||
return string(a)
|
||||
}
|
||||
func (a inmemAddr) Network() string { return "inmem" }
|
||||
|
||||
// String returns the string form
|
||||
func (a dummyAddr) String() string {
|
||||
func (a inmemAddr) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
@@ -23,7 +21,7 @@ func (a dummyAddr) String() string {
|
||||
type InmemSocket struct {
|
||||
chConn chan net.Conn
|
||||
chClose chan struct{}
|
||||
addr dummyAddr
|
||||
addr inmemAddr
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -34,7 +32,7 @@ func NewInmemSocket(addr string, bufSize int) *InmemSocket {
|
||||
return &InmemSocket{
|
||||
chConn: make(chan net.Conn, bufSize),
|
||||
chClose: make(chan struct{}),
|
||||
addr: dummyAddr(addr),
|
||||
addr: inmemAddr(addr),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,15 +65,41 @@ func (s *InmemSocket) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dial is used to establish a connection with the in-mem server.
|
||||
// It returns a [net.ErrClosed] if the connection is already closed.
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
+50
-19
@@ -47,38 +47,69 @@ 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.
|
||||
// By default, socket permissions are 0000 (i.e.: no access for anyone); pass
|
||||
// WithChmod() and WithChown() to set the desired ownership and permissions.
|
||||
// NewUnixSocketWithOpts creates a Unix socket with the specified options.
|
||||
//
|
||||
// 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.
|
||||
// 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].
|
||||
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
|
||||
}
|
||||
|
||||
l, err := listenUnix(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return listenUnix(path, opts...)
|
||||
}
|
||||
|
||||
for _, op := range opts {
|
||||
if err := op(path); err != nil {
|
||||
_ = l.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return l, nil
|
||||
// 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)
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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
@@ -0,0 +1,39 @@
|
||||
//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)
|
||||
}
|
||||
+113
-18
@@ -3,14 +3,35 @@
|
||||
package sockets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// WithChown modifies the socket file's uid and gid
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
@@ -19,8 +40,19 @@ 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
|
||||
}
|
||||
@@ -28,27 +60,90 @@ func WithChmod(mask os.FileMode) SockOption {
|
||||
}
|
||||
}
|
||||
|
||||
// NewUnixSocket creates a unix socket with the specified path and group.
|
||||
// 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.
|
||||
func NewUnixSocket(path string, gid int) (net.Listener, error) {
|
||||
return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660))
|
||||
}
|
||||
|
||||
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.
|
||||
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.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
// listenerBacklog is a caching wrapper around maxListenerBacklog.
|
||||
var listenerBacklog = sync.OnceValue(maxListenerBacklog)
|
||||
|
||||
+24
-10
@@ -48,7 +48,7 @@ func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption
|
||||
}
|
||||
sd, err := getSecurityDescriptor(additionalUsersAndGroups...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("looking up SID: %w", err)
|
||||
return err
|
||||
}
|
||||
return withSDDL(sd)(path)
|
||||
}
|
||||
@@ -85,12 +85,15 @@ 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 {
|
||||
@@ -103,27 +106,38 @@ func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener
|
||||
|
||||
// getSecurityDescriptor returns the DACL for the Unix socket.
|
||||
//
|
||||
// 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.
|
||||
// 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.
|
||||
func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) {
|
||||
sddl := BasePermissions
|
||||
|
||||
// Grant generic read (GR) and write (GW) access to whatever
|
||||
// additional users or groups were specified.
|
||||
//
|
||||
// TODO(thaJeztah): should we fail on, or remove duplicates?
|
||||
// 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
|
||||
for _, g := range additionalUsersAndGroups {
|
||||
sid, err := winio.LookupSidByName(strings.TrimSpace(g))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("looking up SID: %w", err)
|
||||
}
|
||||
sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid)
|
||||
sddl += "(A;;GRGW;;;" + sid + ")"
|
||||
}
|
||||
return sddl, nil
|
||||
}
|
||||
|
||||
func listenUnix(path string) (net.Listener, error) {
|
||||
return net.Listen("unix", path)
|
||||
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
|
||||
}
|
||||
|
||||
+2
-1
@@ -78,7 +78,8 @@ func (b *Broadcaster) configure(ch chan configureRequest, sink Sink) error {
|
||||
select {
|
||||
case ch <- configureRequest{
|
||||
sink: sink,
|
||||
response: response}:
|
||||
response: response,
|
||||
}:
|
||||
ch = nil
|
||||
case err := <-response:
|
||||
return err
|
||||
|
||||
+4
-6
@@ -2,9 +2,7 @@ package events
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
// ErrSinkClosed is returned if a write is issued to a sink that has been
|
||||
// closed. If encountered, the error should be considered terminal and
|
||||
// retries will not be successful.
|
||||
ErrSinkClosed = fmt.Errorf("events: sink closed")
|
||||
)
|
||||
// ErrSinkClosed is returned if a write is issued to a sink that has been
|
||||
// closed. If encountered, the error should be considered terminal and
|
||||
// retries will not be successful.
|
||||
var ErrSinkClosed = fmt.Errorf("events: sink closed")
|
||||
|
||||
+7
-9
@@ -173,15 +173,13 @@ func (b *Breaker) Failure(event Event, err error) bool {
|
||||
return false // never drop events.
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultExponentialBackoffConfig provides a default configuration for
|
||||
// exponential backoff.
|
||||
DefaultExponentialBackoffConfig = ExponentialBackoffConfig{
|
||||
Base: time.Second,
|
||||
Factor: time.Second,
|
||||
Max: 20 * time.Second,
|
||||
}
|
||||
)
|
||||
// DefaultExponentialBackoffConfig provides a default configuration for
|
||||
// exponential backoff.
|
||||
var DefaultExponentialBackoffConfig = ExponentialBackoffConfig{
|
||||
Base: time.Second,
|
||||
Factor: time.Second,
|
||||
Max: 20 * time.Second,
|
||||
}
|
||||
|
||||
// ExponentialBackoffConfig configures backoff parameters.
|
||||
//
|
||||
|
||||
+17
-1
@@ -71,6 +71,7 @@ type ServeMux struct {
|
||||
streamErrorHandler StreamErrorHandlerFunc
|
||||
routingErrorHandler RoutingErrorHandlerFunc
|
||||
disablePathLengthFallback bool
|
||||
disableHTTPMethodOverride bool
|
||||
unescapingMode UnescapingMode
|
||||
writeContentLength bool
|
||||
disableChunkedEncoding bool
|
||||
@@ -271,6 +272,19 @@ func WithDisablePathLengthFallback() ServeMuxOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithDisableHTTPMethodOverride returns a ServeMuxOption that disables the
|
||||
// X-HTTP-Method-Override header handling.
|
||||
//
|
||||
// When this option is used, the mux will no longer allow POST requests with
|
||||
// the X-HTTP-Method-Override header to override the HTTP method. The path
|
||||
// length fallback (POST with application/x-www-form-urlencoded falling back
|
||||
// to a matching GET handler) is not affected by this option.
|
||||
func WithDisableHTTPMethodOverride() ServeMuxOption {
|
||||
return func(serveMux *ServeMux) {
|
||||
serveMux.disableHTTPMethodOverride = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithWriteContentLength returns a ServeMuxOption to enable writing content length on non-streaming responses
|
||||
func WithWriteContentLength() ServeMuxOption {
|
||||
return func(serveMux *ServeMux) {
|
||||
@@ -405,7 +419,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
path = r.URL.RawPath
|
||||
}
|
||||
|
||||
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
|
||||
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && !s.disableHTTPMethodOverride && s.isPathLengthFallback(r) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
_, outboundMarshaler := MarshalerForRequest(s, r)
|
||||
sterr := status.Error(codes.InvalidArgument, err.Error())
|
||||
@@ -467,6 +481,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: mse,
|
||||
})
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -509,6 +524,7 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
HTTPStatus: http.StatusBadRequest,
|
||||
Err: mse,
|
||||
})
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
* -text
|
||||
*.bin -text -diff
|
||||
*.md text eol=lf
|
||||
|
||||
+700
-700
File diff suppressed because it is too large
Load Diff
+78
-78
@@ -1,79 +1,79 @@
|
||||
# Finite State Entropy
|
||||
|
||||
This package provides Finite State Entropy encoding and decoding.
|
||||
|
||||
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
|
||||
encoding provides a fast near-optimal symbol encoding/decoding
|
||||
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
|
||||
|
||||
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
|
||||
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
|
||||
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
|
||||
|
||||
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
|
||||
|
||||
## News
|
||||
|
||||
* Feb 2018: First implementation released. Consider this beta software for now.
|
||||
|
||||
# Usage
|
||||
|
||||
This package provides a low level interface that allows to compress single independent blocks.
|
||||
|
||||
Each block is separate, and there is no built in integrity checks.
|
||||
This means that the caller should keep track of block sizes and also do checksums if needed.
|
||||
|
||||
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
|
||||
You must provide input and will receive the output and maybe an error.
|
||||
|
||||
These error values can be returned:
|
||||
|
||||
| Error | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------|
|
||||
| `<nil>` | Everything ok, output is returned |
|
||||
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
|
||||
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
|
||||
| `(error)` | An internal error occurred. |
|
||||
|
||||
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
|
||||
|
||||
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
|
||||
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
|
||||
object can be used for both.
|
||||
|
||||
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
|
||||
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
|
||||
|
||||
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
|
||||
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
|
||||
your input was likely corrupted.
|
||||
|
||||
It is important to note that a successful decoding does *not* mean your output matches your original input.
|
||||
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
|
||||
|
||||
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
|
||||
|
||||
# Performance
|
||||
|
||||
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
|
||||
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
|
||||
|
||||
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
|
||||
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
|
||||
beneficial to transpose all your input values down by 64.
|
||||
|
||||
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
|
||||
around 300MB/s decompression speed.
|
||||
|
||||
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
|
||||
|
||||
# Plans
|
||||
|
||||
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
|
||||
|
||||
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
|
||||
# Finite State Entropy
|
||||
|
||||
This package provides Finite State Entropy encoding and decoding.
|
||||
|
||||
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
|
||||
encoding provides a fast near-optimal symbol encoding/decoding
|
||||
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
|
||||
|
||||
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
|
||||
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
|
||||
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
|
||||
|
||||
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
|
||||
|
||||
## News
|
||||
|
||||
* Feb 2018: First implementation released. Consider this beta software for now.
|
||||
|
||||
# Usage
|
||||
|
||||
This package provides a low level interface that allows to compress single independent blocks.
|
||||
|
||||
Each block is separate, and there is no built in integrity checks.
|
||||
This means that the caller should keep track of block sizes and also do checksums if needed.
|
||||
|
||||
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
|
||||
You must provide input and will receive the output and maybe an error.
|
||||
|
||||
These error values can be returned:
|
||||
|
||||
| Error | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------|
|
||||
| `<nil>` | Everything ok, output is returned |
|
||||
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
|
||||
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
|
||||
| `(error)` | An internal error occurred. |
|
||||
|
||||
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
|
||||
|
||||
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
|
||||
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
|
||||
object can be used for both.
|
||||
|
||||
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
|
||||
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
|
||||
|
||||
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
|
||||
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
|
||||
your input was likely corrupted.
|
||||
|
||||
It is important to note that a successful decoding does *not* mean your output matches your original input.
|
||||
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
|
||||
|
||||
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
|
||||
|
||||
# Performance
|
||||
|
||||
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
|
||||
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
|
||||
|
||||
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
|
||||
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
|
||||
beneficial to transpose all your input values down by 64.
|
||||
|
||||
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
|
||||
around 300MB/s decompression speed.
|
||||
|
||||
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
|
||||
|
||||
# Plans
|
||||
|
||||
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
|
||||
|
||||
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
|
||||
changes will likely not be accepted. If in doubt open an issue before writing the PR.
|
||||
+89
-89
@@ -1,89 +1,89 @@
|
||||
# Huff0 entropy compression
|
||||
|
||||
This package provides Huff0 encoding and decoding as used in zstd.
|
||||
|
||||
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
|
||||
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
|
||||
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
|
||||
|
||||
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
|
||||
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
|
||||
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
|
||||
|
||||
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
|
||||
|
||||
## News
|
||||
|
||||
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
|
||||
|
||||
This ensures that most functionality is well tested.
|
||||
|
||||
# Usage
|
||||
|
||||
This package provides a low level interface that allows to compress single independent blocks.
|
||||
|
||||
Each block is separate, and there is no built in integrity checks.
|
||||
This means that the caller should keep track of block sizes and also do checksums if needed.
|
||||
|
||||
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
|
||||
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
|
||||
You must provide input and will receive the output and maybe an error.
|
||||
|
||||
These error values can be returned:
|
||||
|
||||
| Error | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------|
|
||||
| `<nil>` | Everything ok, output is returned |
|
||||
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
|
||||
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
|
||||
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
|
||||
| `(error)` | An internal error occurred. |
|
||||
|
||||
|
||||
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
|
||||
|
||||
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
|
||||
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
|
||||
object can be used for both.
|
||||
|
||||
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
|
||||
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
|
||||
|
||||
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
|
||||
|
||||
## Tables and re-use
|
||||
|
||||
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
|
||||
|
||||
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
|
||||
that controls this behaviour. See the documentation for details. This can be altered between each block.
|
||||
|
||||
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
|
||||
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
|
||||
based on the boolean reported back from the CompressXX call.
|
||||
|
||||
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
|
||||
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
|
||||
|
||||
## Decompressing
|
||||
|
||||
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
|
||||
This will initialize the decoding tables.
|
||||
You can supply the complete block to `ReadTable` and it will return the data part of the block
|
||||
which can be given to the decompressor.
|
||||
|
||||
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
|
||||
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
|
||||
|
||||
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
|
||||
|
||||
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
|
||||
your input was likely corrupted.
|
||||
|
||||
It is important to note that a successful decoding does *not* mean your output matches your original input.
|
||||
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
|
||||
changes will likely not be accepted. If in doubt open an issue before writing the PR.
|
||||
# Huff0 entropy compression
|
||||
|
||||
This package provides Huff0 encoding and decoding as used in zstd.
|
||||
|
||||
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
|
||||
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
|
||||
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
|
||||
|
||||
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
|
||||
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
|
||||
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
|
||||
|
||||
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
|
||||
|
||||
## News
|
||||
|
||||
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
|
||||
|
||||
This ensures that most functionality is well tested.
|
||||
|
||||
# Usage
|
||||
|
||||
This package provides a low level interface that allows to compress single independent blocks.
|
||||
|
||||
Each block is separate, and there is no built in integrity checks.
|
||||
This means that the caller should keep track of block sizes and also do checksums if needed.
|
||||
|
||||
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
|
||||
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
|
||||
You must provide input and will receive the output and maybe an error.
|
||||
|
||||
These error values can be returned:
|
||||
|
||||
| Error | Description |
|
||||
|---------------------|-----------------------------------------------------------------------------|
|
||||
| `<nil>` | Everything ok, output is returned |
|
||||
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
|
||||
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
|
||||
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
|
||||
| `(error)` | An internal error occurred. |
|
||||
|
||||
|
||||
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
|
||||
|
||||
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
|
||||
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
|
||||
object can be used for both.
|
||||
|
||||
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
|
||||
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
|
||||
|
||||
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
|
||||
|
||||
## Tables and re-use
|
||||
|
||||
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
|
||||
|
||||
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
|
||||
that controls this behaviour. See the documentation for details. This can be altered between each block.
|
||||
|
||||
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
|
||||
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
|
||||
based on the boolean reported back from the CompressXX call.
|
||||
|
||||
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
|
||||
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
|
||||
|
||||
## Decompressing
|
||||
|
||||
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
|
||||
This will initialize the decoding tables.
|
||||
You can supply the complete block to `ReadTable` and it will return the data part of the block
|
||||
which can be given to the decompressor.
|
||||
|
||||
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
|
||||
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
|
||||
|
||||
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
|
||||
|
||||
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
|
||||
your input was likely corrupted.
|
||||
|
||||
It is important to note that a successful decoding does *not* mean your output matches your original input.
|
||||
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
|
||||
changes will likely not be accepted. If in doubt open an issue before writing the PR.
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
The following versions of go-runewidth are currently supported with
|
||||
security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 0.0.23 | :white_check_mark: |
|
||||
| < 0.0.23 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you discover a security vulnerability in go-runewidth, please report it
|
||||
privately via GitHub's "Report a vulnerability" feature on the Security tab
|
||||
of the repository (https://github.com/mattn/go-runewidth/security), or by
|
||||
emailing the maintainer at mattn.jp@gmail.com.
|
||||
|
||||
Please include a description of the issue, reproduction steps, and the
|
||||
affected version. You can expect an initial response within one week. If
|
||||
the vulnerability is accepted, a fix will be prepared and a new release
|
||||
will be published; you will be credited in the release notes unless you
|
||||
request otherwise. If the report is declined, you will receive an
|
||||
explanation of the reasoning.
|
||||
+118
-35
@@ -2,6 +2,7 @@ package runewidth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -25,13 +26,19 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
zerowidth table // combining + nonprint merged for faster zero-width lookup
|
||||
widewidth table // ambiguous + doublewidth merged for EA path
|
||||
zerowidth table // combining + nonprint merged for faster zero-width lookup
|
||||
widewidth table // ambiguous + doublewidth merged for EA path
|
||||
eastAsianWidth widthTable
|
||||
eastAsianWidth0 [0x300]byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerowidth = mergeIntervals(combining, nonprint)
|
||||
widewidth = mergeIntervals(ambiguous, doublewidth)
|
||||
eastAsianWidth = makeWidthTable(zerowidth, widewidth)
|
||||
for r := range eastAsianWidth0 {
|
||||
eastAsianWidth0[r] = byte(runeWidthEastAsian(rune(r)))
|
||||
}
|
||||
handleEnv()
|
||||
}
|
||||
|
||||
@@ -90,6 +97,14 @@ type interval struct {
|
||||
|
||||
type table []interval
|
||||
|
||||
type widthInterval struct {
|
||||
first rune
|
||||
last rune
|
||||
width byte
|
||||
}
|
||||
|
||||
type widthTable []widthInterval
|
||||
|
||||
func inTable(r rune, t table) bool {
|
||||
if r < t[0].first {
|
||||
return false
|
||||
@@ -116,6 +131,71 @@ func inTable(r rune, t table) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func makeWidthTable(zero, two table) widthTable {
|
||||
wt := make(widthTable, 0, len(zero)+len(two))
|
||||
zi := 0
|
||||
for _, iv := range two {
|
||||
start := iv.first
|
||||
for zi < len(zero) && zero[zi].last < start {
|
||||
zi++
|
||||
}
|
||||
for i := zi; i < len(zero) && zero[i].first <= iv.last; i++ {
|
||||
if start < zero[i].first {
|
||||
wt = append(wt, widthInterval{start, zero[i].first - 1, 2})
|
||||
}
|
||||
if start <= zero[i].last {
|
||||
start = zero[i].last + 1
|
||||
}
|
||||
if start > iv.last {
|
||||
break
|
||||
}
|
||||
}
|
||||
if start <= iv.last {
|
||||
wt = append(wt, widthInterval{start, iv.last, 2})
|
||||
}
|
||||
}
|
||||
for _, iv := range zero {
|
||||
wt = append(wt, widthInterval{iv.first, iv.last, 0})
|
||||
}
|
||||
sort.Slice(wt, func(i, j int) bool {
|
||||
return wt[i].first < wt[j].first
|
||||
})
|
||||
return wt
|
||||
}
|
||||
|
||||
func inWidthTable(r rune, t widthTable) (int, bool) {
|
||||
if r < t[0].first {
|
||||
return 0, false
|
||||
}
|
||||
if r > t[len(t)-1].last {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
bot := 0
|
||||
top := len(t) - 1
|
||||
for top >= bot {
|
||||
mid := (bot + top) >> 1
|
||||
|
||||
switch {
|
||||
case t[mid].last < r:
|
||||
bot = mid + 1
|
||||
case t[mid].first > r:
|
||||
top = mid - 1
|
||||
default:
|
||||
return int(t[mid].width), true
|
||||
}
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func runeWidthEastAsian(r rune) int {
|
||||
if w, ok := inWidthTable(r, eastAsianWidth); ok {
|
||||
return w
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
var private = table{
|
||||
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
|
||||
}
|
||||
@@ -153,13 +233,16 @@ func (c *Condition) RuneWidth(r rune) int {
|
||||
}
|
||||
// optimized version, verified by TestRuneWidthChecksums()
|
||||
if !c.EastAsianWidth {
|
||||
switch {
|
||||
case r < 0x20:
|
||||
if r < 0x20 {
|
||||
return 0
|
||||
case (r >= 0x7F && r <= 0x9F) || r == 0xAD: // nonprint
|
||||
}
|
||||
if (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint
|
||||
return 0
|
||||
case r < 0x300:
|
||||
}
|
||||
if r < 0x300 {
|
||||
return 1
|
||||
}
|
||||
switch {
|
||||
case inTable(r, zerowidth):
|
||||
return 0
|
||||
case inTable(r, doublewidth):
|
||||
@@ -167,20 +250,18 @@ func (c *Condition) RuneWidth(r rune) int {
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case inTable(r, zerowidth):
|
||||
return 0
|
||||
case inTable(r, narrow):
|
||||
return 1
|
||||
case inTable(r, widewidth):
|
||||
return 2
|
||||
case !c.StrictEmojiNeutral && inTable(r, emoji):
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if r < 0x300 {
|
||||
return int(eastAsianWidth0[r])
|
||||
}
|
||||
if w, ok := inWidthTable(r, eastAsianWidth); ok {
|
||||
return w
|
||||
}
|
||||
if !c.StrictEmojiNeutral && inTable(r, emoji) {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
|
||||
@@ -206,6 +287,13 @@ func (c *Condition) CreateLUT() {
|
||||
|
||||
// StringWidth return width as you can see
|
||||
func (c *Condition) StringWidth(s string) (width int) {
|
||||
if len(s) == 1 {
|
||||
b := s[0]
|
||||
if b < 0x20 || b == 0x7F {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
if len(s) > 0 && len(s) <= utf8.UTFMax {
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
if size == len(s) {
|
||||
@@ -213,15 +301,19 @@ func (c *Condition) StringWidth(s string) (width int) {
|
||||
}
|
||||
}
|
||||
// ASCII fast path: no grapheme clustering needed for pure ASCII
|
||||
if isAllASCII(s) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if b >= 0x20 && b != 0x7F {
|
||||
width++
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if b >= 0x80 {
|
||||
goto graphemes
|
||||
}
|
||||
if b >= 0x20 && b != 0x7F {
|
||||
width++
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
graphemes:
|
||||
width = 0
|
||||
g := graphemes.FromString(s)
|
||||
for g.Next() {
|
||||
var chWidth int
|
||||
@@ -236,15 +328,6 @@ func (c *Condition) StringWidth(s string) (width int) {
|
||||
return
|
||||
}
|
||||
|
||||
func isAllASCII(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= 0x80 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Truncate return string truncated with w cells
|
||||
func (c *Condition) Truncate(s string, w int, tail string) string {
|
||||
if c.StringWidth(s) <= w {
|
||||
|
||||
+12
@@ -9,6 +9,7 @@ issues:
|
||||
linters:
|
||||
enable:
|
||||
- errorlint
|
||||
- gosec
|
||||
- unconvert
|
||||
- unparam
|
||||
exclusions:
|
||||
@@ -16,7 +17,18 @@ 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
|
||||
|
||||
+410
-151
@@ -8,13 +8,16 @@ 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"
|
||||
@@ -46,9 +49,18 @@ type (
|
||||
|
||||
// TarOptions wraps the tar options.
|
||||
TarOptions struct {
|
||||
IncludeFiles []string
|
||||
ExcludePatterns []string
|
||||
Compression compression.Compression
|
||||
// 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.
|
||||
NoLchown bool
|
||||
IDMap user.IdentityMapping
|
||||
ChownOpts *ChownOpts
|
||||
@@ -70,9 +82,22 @@ 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.
|
||||
@@ -86,10 +111,33 @@ func NewDefaultArchiver() *Archiver {
|
||||
return &Archiver{Untar: Untar}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 }
|
||||
|
||||
const (
|
||||
AUFSWhiteoutFormat WhiteoutFormat = 0 // AUFSWhiteoutFormat is the default format for whiteouts
|
||||
@@ -98,17 +146,17 @@ const (
|
||||
|
||||
// IsArchivePath checks if the (possibly compressed) file at the given path
|
||||
// starts with a tar file header.
|
||||
func IsArchivePath(path string) bool {
|
||||
file, err := os.Open(path)
|
||||
func IsArchivePath(filePath string) bool {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
defer func() { _ = file.Close() }()
|
||||
rdr, err := compression.DecompressStream(file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer rdr.Close()
|
||||
defer func() { _ = rdr.Close() }()
|
||||
r := tar.NewReader(rdr)
|
||||
_, err = r.Next()
|
||||
return err == nil
|
||||
@@ -129,8 +177,10 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
|
||||
go func() {
|
||||
tarReader := tar.NewReader(inputTarStream)
|
||||
tarWriter := tar.NewWriter(pipeWriter)
|
||||
defer inputTarStream.Close()
|
||||
defer tarWriter.Close()
|
||||
defer func() {
|
||||
_ = tarWriter.Close()
|
||||
_ = inputTarStream.Close()
|
||||
}()
|
||||
|
||||
modify := func(name string, original *tar.Header, modifier TarModifierFunc, tarReader io.Reader) error {
|
||||
header, data, err := modifier(name, original, tarReader)
|
||||
@@ -164,7 +214,7 @@ func ReplaceFileTarWrapper(inputTarStream io.ReadCloser, mods map[string]TarModi
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
pipeWriter.CloseWithError(err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -172,11 +222,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
|
||||
@@ -184,7 +234,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
|
||||
}
|
||||
}
|
||||
@@ -192,12 +242,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
|
||||
}
|
||||
@@ -218,7 +268,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 = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
|
||||
hdr.Mode = chmodTarEntry(hdr.Mode)
|
||||
hdr.Name = canonicalTarName(name, fi.IsDir())
|
||||
return hdr, nil
|
||||
}
|
||||
@@ -227,7 +277,7 @@ const paxSchilyXattr = "SCHILY.xattr."
|
||||
|
||||
// ReadSecurityXattrToTarHeader reads security.capability xattr from filesystem
|
||||
// to a tar header
|
||||
func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
|
||||
func ReadSecurityXattrToTarHeader(filePath string, hdr *tar.Header) error {
|
||||
const (
|
||||
// Values based on linux/include/uapi/linux/capability.h
|
||||
xattrCapsSz2 = 20
|
||||
@@ -235,7 +285,7 @@ func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
|
||||
vfsCapRevision2 = 2
|
||||
vfsCapRevision3 = 3
|
||||
)
|
||||
capability, _ := lgetxattr(path, "security.capability")
|
||||
capability, _ := lgetxattr(filePath, "security.capability")
|
||||
if capability != nil {
|
||||
if capability[versionOffset] == vfsCapRevision3 {
|
||||
// Convert VFS_CAP_REVISION_3 to VFS_CAP_REVISION_2 as root UID makes no
|
||||
@@ -253,7 +303,7 @@ func ReadSecurityXattrToTarHeader(path string, hdr *tar.Header) error {
|
||||
|
||||
type tarWhiteoutConverter interface {
|
||||
ConvertWrite(*tar.Header, string, os.FileInfo) (*tar.Header, error)
|
||||
ConvertRead(*tar.Header, string) (bool, error)
|
||||
ConvertRead(*os.Root, *tar.Header, string) (bool, error)
|
||||
}
|
||||
|
||||
type tarAppender struct {
|
||||
@@ -292,9 +342,10 @@ func canonicalTarName(name string, isDir bool) string {
|
||||
return name
|
||||
}
|
||||
|
||||
// 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)
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -302,17 +353,17 @@ func (ta *tarAppender) addTarFile(path, name string) error {
|
||||
var link string
|
||||
if fi.Mode()&os.ModeSymlink != 0 {
|
||||
var err error
|
||||
link, err = os.Readlink(path)
|
||||
link, err = os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
hdr, err := FileInfoHeader(name, fi, link)
|
||||
hdr, err := FileInfoHeader(archivePath, fi, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ReadSecurityXattrToTarHeader(path, hdr); err != nil {
|
||||
if err := ReadSecurityXattrToTarHeader(srcPath, hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -321,7 +372,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
|
||||
if !fi.IsDir() && hasHardlinks(fi) {
|
||||
inode, err := getInodeFromStat(fi.Sys())
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("unexpected file info for %q: %w", srcPath, err)
|
||||
}
|
||||
// a link should have a name that it links too
|
||||
// and that linked name should be first in the tar archive
|
||||
@@ -330,7 +381,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
|
||||
hdr.Linkname = oldpath
|
||||
hdr.Size = 0 // This Must be here for the writer math to add up!
|
||||
} else {
|
||||
ta.SeenFiles[inode] = name
|
||||
ta.SeenFiles[inode] = hdr.Name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +392,7 @@ func (ta *tarAppender) addTarFile(path, name 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(filepath.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() {
|
||||
if !isOverlayWhiteout && !strings.HasPrefix(path.Base(hdr.Name), WhiteoutPrefix) && !ta.IdentityMapping.Empty() {
|
||||
uid, gid, err := getFileUIDGID(fi.Sys())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -359,7 +410,7 @@ func (ta *tarAppender) addTarFile(path, name string) error {
|
||||
}
|
||||
|
||||
if ta.WhiteoutConverter != nil {
|
||||
wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, path, fi)
|
||||
wo, err := ta.WhiteoutConverter.ConvertWrite(hdr, srcPath, fi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -370,12 +421,12 @@ func (ta *tarAppender) addTarFile(path, name 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
|
||||
}
|
||||
}
|
||||
@@ -387,13 +438,13 @@ func (ta *tarAppender) addTarFile(path, name 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(path)
|
||||
file, err := sequential.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = copyWithBuffer(ta.TarWriter, file)
|
||||
file.Close()
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -402,11 +453,99 @@ func (ta *tarAppender) addTarFile(path, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, opts *TarOptions) error {
|
||||
// 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 {
|
||||
var (
|
||||
Lchown = true
|
||||
inUserns, bestEffortXattrs bool
|
||||
chownOpts *ChownOpts
|
||||
internalOpts *archiveoptions.Options
|
||||
)
|
||||
|
||||
// TODO(thaJeztah): make opts a required argument.
|
||||
@@ -415,6 +554,7 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
|
||||
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,
|
||||
@@ -422,20 +562,35 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
|
||||
// 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 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 {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case tar.TypeReg:
|
||||
// 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())
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -447,47 +602,41 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
|
||||
|
||||
case tar.TypeBlock, tar.TypeChar:
|
||||
if inUserns { // cannot create devices in a userns
|
||||
log.G(context.TODO()).WithFields(log.Fields{"path": path, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
|
||||
log.G(context.TODO()).WithFields(log.Fields{"path": dstPath, "type": hdr.Typeflag}).Debug("skipping device nodes in a userns")
|
||||
return nil
|
||||
}
|
||||
// Handle this is an OS-specific way
|
||||
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
|
||||
if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeFifo:
|
||||
// Handle this is an OS-specific way
|
||||
if err := handleTarTypeBlockCharFifo(hdr, path); err != nil {
|
||||
if err := handleTarTypeBlockCharFifo(root, hdr, dstPath); 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": path, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
|
||||
log.G(context.TODO()).WithFields(log.Fields{"error": err, "path": dstPath, "type": hdr.Typeflag}).Debug("creating fifo node in a userns")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeLink:
|
||||
// #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 {
|
||||
if err := root.Link(hardlinkTarget, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tar.TypeSymlink:
|
||||
// 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.
|
||||
// Symlink targets are archive data, not filesystem paths. Preserve the
|
||||
// target verbatim rather than cleaning or converting it (filepath.FromSlash).
|
||||
linkTarget := hdr.Linkname
|
||||
|
||||
// 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 {
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -504,22 +653,31 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
|
||||
if chownOpts == nil {
|
||||
chownOpts = &ChownOpts{UID: hdr.Uid, GID: hdr.Gid}
|
||||
}
|
||||
if err := os.Lchown(path, chownOpts.UID, chownOpts.GID); err != nil {
|
||||
if err := root.Lchown(dstPath, 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", path, hdr.Uid, hdr.Gid, msg, err)
|
||||
return fmt.Errorf("failed to Lchown %q for UID %d, GID %d%s: %w", dstPath, 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
|
||||
}
|
||||
if err := lsetxattr(path, xattr, []byte(value), 0); err != nil {
|
||||
// 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 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).
|
||||
@@ -538,39 +696,43 @@ func createTarFile(path, extractDir string, hdr *tar.Header, reader io.Reader, o
|
||||
|
||||
// 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(hdr, path, hdrInfo); err != nil {
|
||||
if err := handleLChmod(root, dstPath, hardlinkTarget, hdr, hdrInfo, internalOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
aTime := boundTime(latestTime(hdr.AccessTime, hdr.ModTime))
|
||||
mTime := boundTime(hdr.ModTime)
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
default:
|
||||
// All other file types follow symlinks.
|
||||
if err := chtimes(root, dstPath, aTime, mTime); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tar creates an archive from the directory at `path`, and returns it as a
|
||||
// Tar creates an archive from the directory at `srcPath`, and returns it as a
|
||||
// stream of bytes.
|
||||
func Tar(path string, comp compression.Compression) (io.ReadCloser, error) {
|
||||
return TarWithOptions(path, &TarOptions{Compression: comp})
|
||||
func Tar(srcPath string, comp compression.Compression) (io.ReadCloser, error) {
|
||||
return TarWithOptions(srcPath, &TarOptions{Compression: comp})
|
||||
}
|
||||
|
||||
// TarWithOptions creates an archive from the directory at `path`, only including files whose relative
|
||||
// TarWithOptions creates an archive from the directory at `srcPath`, 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)
|
||||
@@ -639,13 +801,13 @@ func (t *Tarballer) Do() {
|
||||
|
||||
defer func() {
|
||||
// Make sure to check the error on Close.
|
||||
if err := ta.TarWriter.Close(); err != nil {
|
||||
if err := ta.TarWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close tar writer: %s", err)
|
||||
}
|
||||
if err := t.compressWriter.Close(); err != nil {
|
||||
if err := t.compressWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close compress writer: %s", err)
|
||||
}
|
||||
if err := t.pipeWriter.Close(); err != nil {
|
||||
if err := t.pipeWriter.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {
|
||||
log.G(context.TODO()).Errorf("Can't close pipe writer: %s", err)
|
||||
}
|
||||
}()
|
||||
@@ -803,11 +965,28 @@ 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 []*tar.Header
|
||||
var dirs []unpackedDir
|
||||
whiteoutConverter := getWhiteoutConverter(options.WhiteoutFormat)
|
||||
|
||||
// Iterate through the files in the archive.
|
||||
@@ -828,48 +1007,53 @@ loop:
|
||||
continue
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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))
|
||||
}
|
||||
for _, exclude := range options.ExcludePatterns {
|
||||
if strings.HasPrefix(hdr.Name, exclude) {
|
||||
if strings.HasPrefix(name, exclude) {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that the parent directory exists.
|
||||
err = createImpliedDirectories(dest, hdr, options)
|
||||
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))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// #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
|
||||
// If dstPath exists 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 := os.Lstat(path); err == nil {
|
||||
if fi, err := root.Lstat(dstPath); 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", path, dest)
|
||||
return fmt.Errorf("cannot overwrite directory %q with non-directory %q", hdr.Name, 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", path, dest)
|
||||
return fmt.Errorf("cannot overwrite non-directory %q with directory %q", hdr.Name, dest)
|
||||
}
|
||||
|
||||
if fi.IsDir() && hdr.Name == "." {
|
||||
@@ -877,7 +1061,7 @@ loop:
|
||||
}
|
||||
|
||||
if !fi.IsDir() || hdr.Typeflag != tar.TypeDir {
|
||||
if err := os.RemoveAll(path); err != nil {
|
||||
if err := root.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -887,8 +1071,16 @@ 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(hdr, path)
|
||||
writeFile, err := whiteoutConverter.ConvertRead(root, hdr, dstPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -897,51 +1089,121 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
if err := createTarFile(path, dest, hdr, tr, options); err != nil {
|
||||
if err := createTarFile(root, dstPath, 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, hdr)
|
||||
dirs = append(dirs, unpackedDir{hdr: hdr, name: dstPath})
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 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()
|
||||
// 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
|
||||
}
|
||||
|
||||
err = user.MkdirAllAndChown(parentPath, ImpliedDirectoryMode, uid, gid, user.WithOnlyNew)
|
||||
// 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.
|
||||
//
|
||||
// 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)
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,9 +1236,6 @@ 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 {
|
||||
@@ -984,7 +1243,7 @@ func untarHandler(tarArchive io.Reader, dest string, options *TarOptions, decomp
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer decompressedArchive.Close()
|
||||
defer func() { _ = decompressedArchive.Close() }()
|
||||
r = decompressedArchive
|
||||
}
|
||||
|
||||
@@ -998,7 +1257,7 @@ func (archiver *Archiver) TarUntar(src, dst string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer archive.Close()
|
||||
defer func() { _ = archive.Close() }()
|
||||
return archiver.Untar(archive, dst, &TarOptions{
|
||||
IDMap: archiver.IDMapping,
|
||||
})
|
||||
@@ -1010,7 +1269,7 @@ func (archiver *Archiver) UntarPath(src, dst string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer archive.Close()
|
||||
defer func() { _ = archive.Close() }()
|
||||
return archiver.Untar(archive, dst, &TarOptions{
|
||||
IDMap: archiver.IDMapping,
|
||||
})
|
||||
@@ -1070,13 +1329,13 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) {
|
||||
defer close(errC)
|
||||
|
||||
errC <- func() error {
|
||||
defer w.Close()
|
||||
defer func() { _ = w.Close() }()
|
||||
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcF.Close()
|
||||
defer func() { _ = srcF.Close() }()
|
||||
|
||||
hdr, err := tarheader.FileInfoHeaderNoLookups(srcSt, "")
|
||||
if err != nil {
|
||||
@@ -1087,14 +1346,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 = int64(chmodTarEntry(os.FileMode(hdr.Mode)))
|
||||
hdr.Mode = chmodTarEntry(hdr.Mode)
|
||||
|
||||
if err := remapIDs(archiver.IDMapping, hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tw := tar.NewWriter(w)
|
||||
defer tw.Close()
|
||||
defer func() { _ = tw.Close() }()
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1112,7 +1371,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
|
||||
}
|
||||
|
||||
+95
-42
@@ -4,45 +4,71 @@ 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 overlayWhiteoutConverter{}
|
||||
return newOverlayWhiteoutConverter()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type overlayWhiteoutConverter struct{}
|
||||
type overlayWhiteoutConverter struct {
|
||||
opaqueXattr string
|
||||
}
|
||||
|
||||
func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os.FileInfo) (wo *tar.Header, _ error) {
|
||||
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) {
|
||||
// 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 := filepath.Split(hdr.Name)
|
||||
hdr.Name = filepath.Join(dir, WhiteoutPrefix+filename)
|
||||
dir, filename := path.Split(hdr.Name)
|
||||
hdr.Name = path.Join(dir, WhiteoutPrefix+filename)
|
||||
hdr.Mode = 0o600
|
||||
hdr.Typeflag = tar.TypeReg
|
||||
hdr.Size = 0
|
||||
}
|
||||
|
||||
if fi.Mode()&os.ModeDir == 0 {
|
||||
if !fi.IsDir() {
|
||||
// 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(path, opaqueXattrName)
|
||||
opaque, err := lgetxattr(filePath, c.opaqueXattr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -50,14 +76,14 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os
|
||||
// FIXME(thaJeztah): return a sentinel error instead of nil, nil
|
||||
return nil, nil
|
||||
}
|
||||
delete(hdr.PAXRecords, paxSchilyXattr+opaqueXattrName)
|
||||
delete(hdr.PAXRecords, paxSchilyXattr+c.opaqueXattr)
|
||||
|
||||
// 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: filepath.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
|
||||
Name: path.Join(hdr.Name, WhiteoutOpaqueDir), // #nosec G305 -- An archive is being created, not extracted.
|
||||
Size: 0,
|
||||
Uid: hdr.Uid,
|
||||
Uname: hdr.Uname,
|
||||
@@ -68,40 +94,67 @@ func (overlayWhiteoutConverter) ConvertWrite(hdr *tar.Header, path string, fi os
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c overlayWhiteoutConverter) ConvertRead(hdr *tar.Header, path string) (bool, error) {
|
||||
base := filepath.Base(path)
|
||||
dir := filepath.Dir(path)
|
||||
func (c overlayWhiteoutConverter) ConvertRead(root *os.Root, hdr *tar.Header, filePath string) (bool, error) {
|
||||
base := filepath.Base(filePath)
|
||||
dir := filepath.Dir(filePath)
|
||||
|
||||
// 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"
|
||||
}
|
||||
switch base {
|
||||
case WhiteoutPrefix, WhiteoutPrefix + ".", WhiteoutPrefix + "..":
|
||||
return false, fmt.Errorf("invalid whiteout entry %q", hdr.Name)
|
||||
|
||||
err := unix.Setxattr(dir, opaqueXattrName, []byte{'y'}, 0)
|
||||
case WhiteoutOpaqueDir:
|
||||
parent, err := root.Open(dir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("setxattr('%s', %s=y): %w", dir, opaqueXattrName, err)
|
||||
}
|
||||
// don't write the file itself
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 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.Mknod(originalPath, unix.S_IFCHR, 0); err != nil {
|
||||
return false, fmt.Errorf("failed to mknod('%s', S_IFCHR, 0): %w", originalPath, err)
|
||||
}
|
||||
if err := os.Chown(originalPath, hdr.Uid, hdr.Gid); err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer parent.Close()
|
||||
|
||||
// don't write the file itself
|
||||
// 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.
|
||||
originalPath := filepath.Join(dir, originalBase)
|
||||
if err := unix.Mknodat(int(parent.Fd()), originalBase, 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}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't write the whiteout file itself.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user