mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2be9ccfc3 | ||
|
|
0da6a5121f | ||
|
|
6b3ca8fc04 | ||
|
|
f47603c290 | ||
|
|
9709c8fe08 | ||
|
|
af45a23fa9 | ||
|
|
bc97f5ac19 | ||
|
|
6d71967120 | ||
|
|
9b51892b80 |
@@ -4,6 +4,9 @@ https://github.com/docker/cli/blob/master/CONTRIBUTING.md
|
||||
|
||||
** Make sure all your commits include a signature generated with `git commit -s` **
|
||||
|
||||
For additional information on our contributing process, read our contributing
|
||||
guide https://docs.docker.com/opensource/code/
|
||||
|
||||
If this is a bug fix, make sure your description includes "fixes #xxxx", or
|
||||
"closes #xxxx"
|
||||
|
||||
@@ -17,7 +20,6 @@ Provide the following information:
|
||||
**- How to verify it**
|
||||
|
||||
**- Human readable description for the release notes**
|
||||
|
||||
<!--
|
||||
Write a short (one line) summary that describes the changes in this
|
||||
pull request for inclusion in the changelog.
|
||||
@@ -26,9 +28,10 @@ It must be placed inside the below triple backticks section.
|
||||
NOTE: Only fill this section if changes introduced in this PR are user-facing.
|
||||
The PR must have a relevant impact/ label.
|
||||
-->
|
||||
|
||||
```markdown changelog
|
||||
|
||||
|
||||
```
|
||||
|
||||
**- A picture of a cute animal (not mandatory but encouraged)**
|
||||
|
||||
|
||||
@@ -5,14 +5,7 @@ updates:
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "area/ci"
|
||||
- "area/testing"
|
||||
- "status/2-code-review"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
codeql-actions:
|
||||
patterns:
|
||||
- "github/codeql-action/*"
|
||||
docker-actions:
|
||||
patterns:
|
||||
- "docker/*"
|
||||
|
||||
+48
-39
@@ -35,9 +35,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -65,10 +63,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
set: |
|
||||
@@ -80,50 +78,63 @@ jobs:
|
||||
working-directory: ./build
|
||||
run: |
|
||||
mkdir /tmp/out
|
||||
platformPair=${PLATFORM//\//-}
|
||||
platform=${{ matrix.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.0.1
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
||||
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' }}
|
||||
uses: docker/github-builder/.github/workflows/bake.yml@a492c6d04fd3315f67230809b44d60cc0acd50b3 # v1.16.0
|
||||
permissions:
|
||||
contents: read # same as global permission
|
||||
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token
|
||||
with:
|
||||
setup-qemu: true
|
||||
target: bin-image-cross
|
||||
cache: true
|
||||
cache-scope: bin-image
|
||||
output: image
|
||||
push: true
|
||||
vars: |
|
||||
VERSION=${{ github.ref }}
|
||||
meta-images: |
|
||||
dockereng/cli-bin
|
||||
meta-tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
registry-identities: |
|
||||
- type: dockerhub
|
||||
username: dockereng
|
||||
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }}
|
||||
steps:
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
|
||||
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@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
files: |
|
||||
./docker-bake.hcl
|
||||
cwd://${{ steps.meta.outputs.bake-file }}
|
||||
targets: bin-image-cross
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
set: |
|
||||
*.cache-from=type=gha,scope=bin-image
|
||||
*.cache-to=type=gha,scope=bin-image,mode=max
|
||||
|
||||
prepare-plugins:
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -132,9 +143,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Create matrix
|
||||
id: platforms
|
||||
@@ -156,10 +165,10 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -46,10 +46,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
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
|
||||
@@ -62,20 +61,20 @@ jobs:
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version: "1.26.7"
|
||||
go-version: "1.25.8"
|
||||
cache: false
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -44,9 +44,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Update daemon.json
|
||||
run: |
|
||||
@@ -65,7 +63,7 @@ jobs:
|
||||
docker info
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Run ${{ matrix.target }}
|
||||
run: |
|
||||
@@ -76,7 +74,7 @@ jobs:
|
||||
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
@@ -1,23 +0,0 @@
|
||||
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) || '' }}
|
||||
@@ -1,157 +0,0 @@
|
||||
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@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -60,15 +60,14 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
path: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
persist-credentials: false
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
||||
with:
|
||||
go-version: "1.26.7"
|
||||
go-version: "1.25.8"
|
||||
cache: false
|
||||
-
|
||||
name: Test
|
||||
@@ -82,7 +81,7 @@ jobs:
|
||||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
|
||||
with:
|
||||
files: /tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
name: validate-milestone
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, milestoned, demilestoned, edited]
|
||||
|
||||
jobs:
|
||||
validate-milestone:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Validate milestone matches VERSION
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
MILESTONE: ${{ github.event.pull_request.milestone.title }}
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
});
|
||||
core.info(`Modified files: ${files.map(f => f.filename).join(', ')}`);
|
||||
|
||||
const touchesVersion = files.some(f => f.filename === 'VERSION');
|
||||
core.info(`Touches VERSION: ${touchesVersion}`);
|
||||
|
||||
// Use the PR's version when it bumps the file, base branch otherwise.
|
||||
// It's fine to trust the author in this case, it's not meant to be
|
||||
// a security gate, just a helpful check for maintainers.
|
||||
const ref = touchesVersion
|
||||
? context.payload.pull_request.head.sha
|
||||
: context.payload.pull_request.base.ref;
|
||||
|
||||
core.info(`Base ref: ${ref}`);
|
||||
|
||||
const resp = await github.rest.repos.getContent({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
path: 'VERSION',
|
||||
ref,
|
||||
});
|
||||
const expected = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8').trim();
|
||||
const milestone = process.env.MILESTONE;
|
||||
|
||||
if (!milestone) {
|
||||
core.setFailed(`PR must have a milestone set (expected: ${expected})`);
|
||||
return;
|
||||
}
|
||||
if (milestone !== expected) {
|
||||
core.setFailed(`Milestone '${milestone}' does not match VERSION '${expected}'`);
|
||||
return;
|
||||
}
|
||||
core.info(`Milestone: ${milestone} ✓`);
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@82490499d2e5613fcead7e128237ef0b0ea210f7 # v7
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
@@ -48,9 +48,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Generate
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
@@ -76,36 +74,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
-
|
||||
name: Run
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
run: |
|
||||
make -f docker.Makefile ${{ matrix.target }}
|
||||
|
||||
validate-gocompat:
|
||||
runs-on: ubuntu-24.04
|
||||
env:
|
||||
GOPATH: ${{ github.workspace }}
|
||||
GO111MODULE: off
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
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@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
with:
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
-
|
||||
name: Run gocompat check
|
||||
shell: 'script --return --quiet --command "bash {0}"'
|
||||
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
|
||||
run: |
|
||||
make -C ./internal/gocompat verify
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
+12
-11
@@ -5,7 +5,7 @@ run:
|
||||
# which causes it to fallback to go1.17 semantics.
|
||||
#
|
||||
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
|
||||
go: "1.26.7"
|
||||
go: "1.25.8"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@@ -110,15 +110,8 @@ linters:
|
||||
excludes:
|
||||
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore)
|
||||
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
|
||||
- G117 # G117: Exported struct field matches secret pattern (false positives for legitimate field names)
|
||||
- G118 # G118: Goroutine uses context.Background/TODO while request-scoped context is available (TODO: evaluate these)
|
||||
- G122 # G122: Filesystem operation in filepath.Walk/WalkDir callback uses race-prone path (TODO: evaluate these)
|
||||
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
|
||||
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
|
||||
- G702 # G702: Command injection via taint analysis (TODO: evaluate these)
|
||||
- G703 # G703: Path traversal via taint analysis (TODO: evaluate these)
|
||||
- G704 # G704: SSRF via taint analysis (TODO: evaluate these)
|
||||
- G705 # G705: XSS via taint analysis (TODO: evaluate these)
|
||||
|
||||
govet:
|
||||
enable:
|
||||
@@ -165,6 +158,11 @@ linters:
|
||||
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
|
||||
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
|
||||
|
||||
usetesting:
|
||||
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
context-background: false # FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
context-todo: false # FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
|
||||
|
||||
exclusions:
|
||||
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
|
||||
# automatically inherited. We can decide whether or not to follow upstream
|
||||
@@ -227,10 +225,13 @@ linters:
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
|
||||
- text: "compiler directive unrecognized: //go:fix"
|
||||
# Ignore deprecation linting for cli/command/stack/*.
|
||||
#
|
||||
# FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389
|
||||
- text: '^(SA1019): '
|
||||
path: "cli/command/stack"
|
||||
linters:
|
||||
- gocheckcompilerdirectives
|
||||
- staticcheck
|
||||
|
||||
# Log a warning if an exclusion rule is unused.
|
||||
# Default: false
|
||||
|
||||
@@ -39,7 +39,6 @@ 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>
|
||||
@@ -356,9 +355,7 @@ Louis Opter <kalessin@kalessin.fr>
|
||||
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
|
||||
Lovekesh Kumar <lovekesh.kumar@rtcamp.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>
|
||||
@@ -405,7 +402,6 @@ 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>
|
||||
@@ -575,8 +571,6 @@ 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>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# This file lists all contributors to the repository.
|
||||
# See scripts/docs/generate-authors.sh to make modifications.
|
||||
|
||||
4RH1T3CT0R7 <iprintercanon@gmail.com>
|
||||
A. Lester Buck III <github-reg@nbolt.com>
|
||||
Aanand Prasad <aanand.prasad@gmail.com>
|
||||
Aaron L. Xu <liker.xu@foxmail.com>
|
||||
@@ -43,8 +42,6 @@ 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>
|
||||
Alfred Landrum <alfred.landrum@docker.com>
|
||||
@@ -67,7 +64,6 @@ Andres G. Aragoneses <knocte@gmail.com>
|
||||
Andres Leon Rangel <aleon1220@gmail.com>
|
||||
Andrew France <andrew@avito.co.uk>
|
||||
Andrew He <he.andrew.mail@gmail.com>
|
||||
Andrew Hopp <andrew.hopp@me.com>
|
||||
Andrew Hsu <andrewhsu@docker.com>
|
||||
Andrew Macpherson <hopscotch23@gmail.com>
|
||||
Andrew McDonnell <bugs@andrewmcdonnell.net>
|
||||
@@ -131,7 +127,6 @@ Brian Goff <cpuguy83@gmail.com>
|
||||
Brian Tracy <brian.tracy33@gmail.com>
|
||||
Brian Wieder <brian@4wieders.com>
|
||||
Bruno Sousa <bruno.sousa@docker.com>
|
||||
Bruno Verachten <gounthar@gmail.com>
|
||||
Bryan Bess <squarejaw@bsbess.com>
|
||||
Bryan Boreham <bjboreham@gmail.com>
|
||||
Bryan Murphy <bmurphy1976@gmail.com>
|
||||
@@ -162,7 +157,6 @@ 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>
|
||||
@@ -184,7 +178,6 @@ Christopher Svensson <stoffus@stoffus.com>
|
||||
Christy Norman <christy@linux.vnet.ibm.com>
|
||||
Chun Chen <ramichen@tencent.com>
|
||||
Clinton Kitson <clintonskitson@gmail.com>
|
||||
Codex <codex@openai.com>
|
||||
Coenraad Loubser <coenraad@wish.org.za>
|
||||
Colin Hebert <hebert.colin@gmail.com>
|
||||
Collin Guarino <collin.guarino@gmail.com>
|
||||
@@ -241,7 +234,6 @@ David Sheets <dsheets@docker.com>
|
||||
David Williamson <david.williamson@docker.com>
|
||||
David Xia <dxia@spotify.com>
|
||||
David Young <yangboh@cn.ibm.com>
|
||||
Davlat Davydov <literally_user@hotmail.com>
|
||||
Deng Guangxing <dengguangxing@huawei.com>
|
||||
Denis Defreyne <denis@soundcloud.com>
|
||||
Denis Gladkikh <denis@gladkikh.email>
|
||||
@@ -249,7 +241,6 @@ Denis Ollier <larchunix@users.noreply.github.com>
|
||||
Dennis Docter <dennis@d23.nl>
|
||||
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
|
||||
Derek McGowan <derek@mcg.dev>
|
||||
Derek Misler <derek.misler@docker.com>
|
||||
Des Preston <despreston@gmail.com>
|
||||
Deshi Xiao <dxiao@redhat.com>
|
||||
Dharmit Shah <shahdharmit@gmail.com>
|
||||
@@ -269,7 +260,6 @@ Dominik Braun <dominik.braun@nbsp.de>
|
||||
Don Kjer <don.kjer@gmail.com>
|
||||
Dong Chen <dongluo.chen@docker.com>
|
||||
DongGeon Lee <secmatth1996@gmail.com>
|
||||
Dorin Geman <dorin.geman@docker.com>
|
||||
Doug Davis <dug@us.ibm.com>
|
||||
Drew Erny <derny@mirantis.com>
|
||||
Ed Costello <epc@epcostello.com>
|
||||
@@ -481,7 +471,6 @@ Justyn Temme <justyntemme@gmail.com>
|
||||
Jyrki Puttonen <jyrkiput@gmail.com>
|
||||
Jérémie Drouet <jeremie.drouet@gmail.com>
|
||||
Jérôme Petazzoni <jerome.petazzoni@docker.com>
|
||||
Jörg Sommer <joerg@jo-so.de>
|
||||
Jörg Thalheim <joerg@higgsboson.tk>
|
||||
Kai Blin <kai@samba.org>
|
||||
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
|
||||
@@ -550,13 +539,10 @@ Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
|
||||
Luca Favatella <luca.favatella@erlang-solutions.com>
|
||||
Luca Marturana <lucamarturana@gmail.com>
|
||||
Lucas Chan <lucas-github@lucaschan.com>
|
||||
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>
|
||||
Lénaïc Huard <lhuard@amadeus.com>
|
||||
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
|
||||
@@ -565,7 +551,6 @@ 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>
|
||||
@@ -593,12 +578,10 @@ 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>
|
||||
@@ -620,10 +603,8 @@ Michael Spetsiotis <michael_spets@hotmail.com>
|
||||
Michael Steinert <mike.steinert@gmail.com>
|
||||
Michael Tews <michael@tews.dev>
|
||||
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>
|
||||
@@ -636,7 +617,6 @@ Mike Goelzer <mike.goelzer@docker.com>
|
||||
Mike MacCana <mike.maccana@gmail.com>
|
||||
mikelinjie <294893458@qq.com>
|
||||
Mikhail Vasin <vasin@cloud-tv.ru>
|
||||
Milas Bowman <milas.bowman@docker.com>
|
||||
Milind Chawre <milindchawre@gmail.com>
|
||||
Mindaugas Rukas <momomg@gmail.com>
|
||||
Miroslav Gula <miroslav.gula@naytrolabs.com>
|
||||
@@ -645,7 +625,6 @@ 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>
|
||||
@@ -697,7 +676,6 @@ 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>
|
||||
@@ -724,7 +702,6 @@ 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>
|
||||
@@ -749,7 +726,6 @@ 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>
|
||||
@@ -800,7 +776,6 @@ 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>
|
||||
@@ -901,10 +876,8 @@ 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>
|
||||
@@ -914,7 +887,6 @@ Vincent Batts <vbatts@redhat.com>
|
||||
Vincent Bernat <Vincent.Bernat@exoscale.ch>
|
||||
Vincent Demeester <vincent.demeester@docker.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
Vineet Kumar <vineetkumar17112004@gmail.com>
|
||||
Vishnu Kannan <vishnuk@google.com>
|
||||
Vivek Goyal <vgoyal@redhat.com>
|
||||
Wang Jie <wangjie5@chinaskycloud.com>
|
||||
@@ -944,7 +916,6 @@ Yanqiang Miao <miao.yanqiang@zte.com.cn>
|
||||
Yassine Tijani <yasstij11@gmail.com>
|
||||
Yi EungJun <eungjun.yi@navercorp.com>
|
||||
Ying Li <ying.li@docker.com>
|
||||
Yoan Wainmann <thebook90yw@gmail.com>
|
||||
Yong Tang <yong.tang.github@outlook.com>
|
||||
Yosef Fertel <yfertel@gmail.com>
|
||||
Yu Peng <yu.peng36@zte.com.cn>
|
||||
|
||||
+6
-9
@@ -66,7 +66,7 @@ anybody starts working on it.
|
||||
We are always thrilled to receive pull requests. We do our best to process them
|
||||
quickly. If your pull request is not accepted on the first try,
|
||||
don't get discouraged! Our contributor's guide explains [the review process we
|
||||
use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWING.md).
|
||||
use for simple changes](https://github.com/docker/docker/blob/master/project/REVIEWING.md).
|
||||
|
||||
### Talking to other Docker users and contributors
|
||||
|
||||
@@ -124,7 +124,8 @@ submitting a pull request.
|
||||
Update the documentation when creating or modifying features. Test your
|
||||
documentation changes for clarity, concision, and correctness, as well as a
|
||||
clean documentation build. See our contributors guide for [our style
|
||||
guide](https://github.com/docker/docs/blob/main/STYLE.md).
|
||||
guide](https://docs.docker.com/contribute/style/grammar/) and instructions on [building
|
||||
the documentation](https://docs.docker.com/contribute/).
|
||||
|
||||
Write clean code. Universally formatted code promotes ease of writing, reading,
|
||||
and maintenance. Always run `gofmt -s -w file.go` on each changed file before
|
||||
@@ -144,7 +145,6 @@ not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
|
||||
or `telemetry: <message>`.
|
||||
|
||||
A standard commit.
|
||||
|
||||
```
|
||||
Fix the exploding flux capacitor
|
||||
|
||||
@@ -153,7 +153,6 @@ the sun and the moon align.
|
||||
```
|
||||
|
||||
Using a package as prefix.
|
||||
|
||||
```
|
||||
pkg/foo: prevent panic in flux capacitor
|
||||
|
||||
@@ -161,14 +160,12 @@ Calling function A causes the flux capacitor to blow up every time
|
||||
the sun and the moon align.
|
||||
```
|
||||
|
||||
Updating a specific vendored dependency.
|
||||
|
||||
Updating a specific vendored package.
|
||||
```
|
||||
vendor: github.com/moby/moby/client v0.4.0
|
||||
vendor: github.com/docker/docker 6ac445c42bad (master, v28.0-dev)
|
||||
```
|
||||
|
||||
Fixing a broken docs link.
|
||||
|
||||
```
|
||||
docs: fix style/lint issues in deprecated.md
|
||||
```
|
||||
@@ -267,7 +264,7 @@ Then you just add a line to every git commit message:
|
||||
|
||||
Signed-off-by: Joe Smith <joe.smith@email.com>
|
||||
|
||||
Use a known identity (sorry, no anonymous contributions.)
|
||||
Use your real name (sorry, no pseudonyms or anonymous contributions.)
|
||||
|
||||
If you set your `user.name` and `user.email` git configs, you can sign your
|
||||
commit automatically with `git commit -s`.
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.7
|
||||
ARG GO_VERSION=1.25.8
|
||||
|
||||
# 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.
|
||||
@@ -25,12 +25,12 @@ ARG GOTESTSUM_VERSION=v1.13.0
|
||||
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
|
||||
# It must be a tag in the docker.io/docker/buildx-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG BUILDX_VERSION=0.34.1
|
||||
ARG BUILDX_VERSION=0.31.1
|
||||
|
||||
# COMPOSE_VERSION is the version of compose to install in the dev container.
|
||||
# It must be a tag in the docker.io/docker/compose-bin image repository
|
||||
# on Docker Hub.
|
||||
ARG COMPOSE_VERSION=v5.1.3
|
||||
ARG COMPOSE_VERSION=v5.1.0
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[](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)
|
||||
|
||||
@@ -50,18 +51,6 @@ Run test:
|
||||
docker buildx bake test
|
||||
```
|
||||
|
||||
Run the unit test:
|
||||
|
||||
```
|
||||
$ make -f docker.Makefile test-unit
|
||||
```
|
||||
|
||||
Run the full test suite:
|
||||
|
||||
```
|
||||
$ make -f docker.Makefile test
|
||||
```
|
||||
|
||||
List all the available targets:
|
||||
|
||||
```shell
|
||||
@@ -73,7 +62,7 @@ make help
|
||||
Start an interactive development environment:
|
||||
|
||||
```shell
|
||||
make shell
|
||||
make -f docker.Makefile shell
|
||||
```
|
||||
|
||||
## Legal
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
|
||||
// Package hooks defines the contract between the Docker CLI and CLI plugin hook
|
||||
// implementations.
|
||||
//
|
||||
// # Audience
|
||||
//
|
||||
// This package is intended to be imported by CLI plugin implementations that
|
||||
// implement a "hooks" subcommand, and by the Docker CLI when invoking those
|
||||
// hooks.
|
||||
//
|
||||
// # Contract and wire format
|
||||
//
|
||||
// Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook
|
||||
// subcommand (currently as a command-line argument). Hook outputs are emitted by
|
||||
// the plugin as JSON (see [Response]).
|
||||
//
|
||||
// # Stability
|
||||
//
|
||||
// The types that represent the hook contract ([Request], [Response] and related
|
||||
// constants) are considered part of Docker CLI's public Go API.
|
||||
// Fields and values may be extended in a backwards-compatible way (for example,
|
||||
// adding new fields), but existing fields and their meaning should remain stable.
|
||||
// Plugins should ignore unknown fields and unknown hook types to remain
|
||||
// forwards-compatible.
|
||||
package hooks
|
||||
|
||||
// ResponseType is the type of response from the plugin.
|
||||
type ResponseType int
|
||||
|
||||
const (
|
||||
NextSteps ResponseType = 0
|
||||
)
|
||||
|
||||
// Request is the type representing the information
|
||||
// that plugins declaring support for hooks get passed when
|
||||
// being invoked following a CLI command execution.
|
||||
type Request struct {
|
||||
// RootCmd is a string representing the matching hook configuration
|
||||
// which is currently being invoked. If a hook for "docker context"
|
||||
// is configured and the user executes "docker context ls", the plugin
|
||||
// is invoked with "context".
|
||||
RootCmd string `json:"RootCmd,omitzero"`
|
||||
|
||||
// Flags contains flags that were set on the command for which the
|
||||
// hook was invoked. It uses flag names as key, with leading hyphens
|
||||
// removed ("--flag" and "-flag" are included as "flag" and "f").
|
||||
//
|
||||
// Flag values are not included and are set to an empty string,
|
||||
// except for boolean flags known to the CLI itself, for which
|
||||
// the value is either "true", or "false".
|
||||
//
|
||||
// Plugins can use this information to adjust their [Response]
|
||||
// based on whether the command triggering the hook was invoked
|
||||
// with.
|
||||
Flags map[string]string `json:"Flags,omitzero"`
|
||||
|
||||
// CommandError is a string containing the error output (if any)
|
||||
// of the command for which the hook was invoked.
|
||||
CommandError string `json:"CommandError,omitzero"`
|
||||
}
|
||||
|
||||
// Response represents a plugin hook response. Plugins
|
||||
// declaring support for CLI hooks need to print a JSON
|
||||
// representation of this type when their hook subcommand
|
||||
// is invoked.
|
||||
type Response struct {
|
||||
Type ResponseType `json:"Type"`
|
||||
Template string `json:"Template,omitzero"`
|
||||
}
|
||||
|
||||
// HookType is the type of response from the plugin.
|
||||
//
|
||||
// Deprecated: use [ResponseType] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
type HookType = ResponseType
|
||||
|
||||
// HookMessage represents a plugin hook response.
|
||||
//
|
||||
// Deprecated: use [Response] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
type HookMessage = Response
|
||||
@@ -1,75 +0,0 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
hookTemplateCommandName = `{{command}}`
|
||||
hookTemplateFlagValue = `{{flagValue %q}}`
|
||||
hookTemplateArg = `{{argValue %d}}`
|
||||
)
|
||||
|
||||
// TemplateReplaceSubcommandName returns a hook template string
|
||||
// that will be replaced by the CLI subcommand being executed
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Response{
|
||||
// Type: NextSteps,
|
||||
// Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(),
|
||||
// }
|
||||
//
|
||||
// When being executed after the command:
|
||||
//
|
||||
// docker run --name "my-container" alpine
|
||||
//
|
||||
// It results in the message:
|
||||
//
|
||||
// you ran the subcommand: run
|
||||
func TemplateReplaceSubcommandName() string {
|
||||
return hookTemplateCommandName
|
||||
}
|
||||
|
||||
// TemplateReplaceFlagValue returns a hook template string that will be
|
||||
// replaced with the flags value when printed by the CLI.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Response{
|
||||
// Type: NextSteps,
|
||||
// Template: "you ran a container named: " + TemplateReplaceFlagValue("name"),
|
||||
// }
|
||||
//
|
||||
// when executed after the command:
|
||||
//
|
||||
// docker run --name "my-container" alpine
|
||||
//
|
||||
// it results in the message:
|
||||
//
|
||||
// you ran a container named: my-container
|
||||
func TemplateReplaceFlagValue(flag string) string {
|
||||
return fmt.Sprintf(hookTemplateFlagValue, flag)
|
||||
}
|
||||
|
||||
// TemplateReplaceArg takes an index i and returns a hook
|
||||
// template string that the CLI will replace the template with
|
||||
// the ith argument after processing the passed flags.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Response{
|
||||
// Type: NextSteps,
|
||||
// Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`",
|
||||
// }
|
||||
//
|
||||
// when being executed after the command:
|
||||
//
|
||||
// docker pull alpine
|
||||
//
|
||||
// It results in the message:
|
||||
//
|
||||
// Run this image with `docker run alpine`
|
||||
func TemplateReplaceArg(i int) string {
|
||||
return fmt.Sprintf(hookTemplateArg, i)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package hooks_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
)
|
||||
|
||||
func TestTemplateHelpers(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
got func() string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
doc: "subcommand name",
|
||||
got: hooks.TemplateReplaceSubcommandName,
|
||||
want: `{{command}}`,
|
||||
},
|
||||
{
|
||||
doc: "flag value",
|
||||
got: func() string {
|
||||
return hooks.TemplateReplaceFlagValue("name")
|
||||
},
|
||||
want: `{{flagValue "name"}}`,
|
||||
},
|
||||
{
|
||||
doc: "arg",
|
||||
got: func() string {
|
||||
return hooks.TemplateReplaceArg(0)
|
||||
},
|
||||
want: `{{argValue 0}}`,
|
||||
},
|
||||
{
|
||||
doc: "arg",
|
||||
got: func() string {
|
||||
return hooks.TemplateReplaceArg(3)
|
||||
},
|
||||
want: `{{argValue 3}}`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
if got := tc.got(); got != tc.want {
|
||||
t.Fatalf("expected %q, got %q", tc.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,18 @@
|
||||
package hooks
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
const (
|
||||
whatsNext = "\n\033[1mWhat's next:\033[0m\n"
|
||||
indent = " "
|
||||
"github.com/morikuni/aec"
|
||||
)
|
||||
|
||||
// PrintNextSteps renders list of [NextSteps] messages and writes them
|
||||
// to out. It is a no-op if messages is empty.
|
||||
func PrintNextSteps(out io.Writer, messages []string) {
|
||||
if len(messages) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(out, whatsNext)
|
||||
for _, msg := range messages {
|
||||
_, _ = io.WriteString(out, indent)
|
||||
_, _ = io.WriteString(out, msg)
|
||||
_, _ = io.WriteString(out, "\n")
|
||||
_, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
|
||||
for _, n := range messages {
|
||||
_, _ = fmt.Fprintln(out, " ", n)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,38 @@
|
||||
package hooks_test
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/morikuni/aec"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestPrintHookMessages(t *testing.T) {
|
||||
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
|
||||
|
||||
tests := []struct {
|
||||
doc string
|
||||
testCases := []struct {
|
||||
messages []string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
doc: "no messages",
|
||||
messages: nil,
|
||||
messages: []string{},
|
||||
expectedOutput: "",
|
||||
},
|
||||
{
|
||||
doc: "single message",
|
||||
messages: []string{"Bork!"},
|
||||
expectedOutput: header +
|
||||
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||
" Bork!\n",
|
||||
},
|
||||
{
|
||||
doc: "multiple messages",
|
||||
messages: []string{"Foo", "bar"},
|
||||
expectedOutput: header +
|
||||
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
|
||||
" Foo\n" +
|
||||
" bar\n",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
var w strings.Builder
|
||||
hooks.PrintNextSteps(&w, tc.messages)
|
||||
assert.Equal(t, w.String(), tc.expectedOutput)
|
||||
})
|
||||
|
||||
for _, tc := range testCases {
|
||||
w := bytes.Buffer{}
|
||||
PrintNextSteps(&w, tc.messages)
|
||||
assert.Equal(t, w.String(), tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,116 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const maxMessages = 10
|
||||
type HookType int
|
||||
|
||||
const (
|
||||
NextSteps = iota
|
||||
)
|
||||
|
||||
// HookMessage represents a plugin hook response. Plugins
|
||||
// declaring support for CLI hooks need to print a json
|
||||
// representation of this type when their hook subcommand
|
||||
// is invoked.
|
||||
type HookMessage struct {
|
||||
Type HookType
|
||||
Template string
|
||||
}
|
||||
|
||||
// TemplateReplaceSubcommandName returns a hook template string
|
||||
// that will be replaced by the CLI subcommand being executed
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
|
||||
//
|
||||
// when being executed after the command:
|
||||
// `docker run --name "my-container" alpine`
|
||||
// will result in the message:
|
||||
// `you ran the subcommand: run`
|
||||
func TemplateReplaceSubcommandName() string {
|
||||
return hookTemplateCommandName
|
||||
}
|
||||
|
||||
// TemplateReplaceFlagValue returns a hook template string
|
||||
// that will be replaced by the flags value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "you ran a container named: " + TemplateReplaceFlagValue("name")
|
||||
//
|
||||
// when being executed after the command:
|
||||
// `docker run --name "my-container" alpine`
|
||||
// will result in the message:
|
||||
// `you ran a container named: my-container`
|
||||
func TemplateReplaceFlagValue(flag string) string {
|
||||
return fmt.Sprintf(hookTemplateFlagValue, flag)
|
||||
}
|
||||
|
||||
// TemplateReplaceArg takes an index i and returns a hook
|
||||
// template string that the CLI will replace the template with
|
||||
// the ith argument, after processing the passed flags.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
|
||||
//
|
||||
// when being executed after the command:
|
||||
// `docker pull alpine`
|
||||
// will result in the message:
|
||||
// "Run this image with `docker run alpine`"
|
||||
func TemplateReplaceArg(i int) string {
|
||||
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
|
||||
}
|
||||
|
||||
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
|
||||
out := hookTemplate
|
||||
if strings.Contains(hookTemplate, "{{") {
|
||||
// Message may be a template.
|
||||
msgContext := commandInfo{cmd: cmd}
|
||||
|
||||
tmpl, err := template.New("").Funcs(template.FuncMap{
|
||||
"command": msgContext.command,
|
||||
"flagValue": msgContext.flagValue,
|
||||
"argValue": msgContext.argValue,
|
||||
|
||||
// kept for backward-compatibility with old templates.
|
||||
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
|
||||
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
|
||||
}).Parse(hookTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var b bytes.Buffer
|
||||
err = tmpl.Execute(&b, msgContext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = b.String()
|
||||
tmpl := template.New("").Funcs(commandFunctions)
|
||||
tmpl, err := tmpl.Parse(hookTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n := strings.Count(out, "\n"); n > maxMessages {
|
||||
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
|
||||
b := bytes.Buffer{}
|
||||
err = tmpl.Execute(&b, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strings.SplitN(out, "\n", maxMessages), nil
|
||||
return strings.Split(b.String(), "\n"), nil
|
||||
}
|
||||
|
||||
var ErrHookTemplateParse = errors.New("failed to parse hook template")
|
||||
|
||||
// commandInfo provides info about the command for which the hook was invoked.
|
||||
// It is used for templated hook-messages.
|
||||
type commandInfo struct {
|
||||
cmd *cobra.Command
|
||||
const (
|
||||
hookTemplateCommandName = "{{.Name}}"
|
||||
hookTemplateFlagValue = `{{flag . "%s"}}`
|
||||
hookTemplateArg = "{{arg . %s}}"
|
||||
)
|
||||
|
||||
var commandFunctions = template.FuncMap{
|
||||
"flag": getFlagValue,
|
||||
"arg": getArgValue,
|
||||
}
|
||||
|
||||
// Name returns the name of the (sub)command for which the hook was invoked.
|
||||
//
|
||||
// It's used for backward-compatibility with old templates.
|
||||
func (c commandInfo) Name() string {
|
||||
return c.command()
|
||||
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
|
||||
cmdFlag := cmd.Flag(flag)
|
||||
if cmdFlag == nil {
|
||||
return "", ErrHookTemplateParse
|
||||
}
|
||||
return cmdFlag.Value.String(), nil
|
||||
}
|
||||
|
||||
// command returns the name of the (sub)command for which the hook was invoked.
|
||||
func (c commandInfo) command() string {
|
||||
if c.cmd == nil {
|
||||
return ""
|
||||
func getArgValue(cmd *cobra.Command, i int) (string, error) {
|
||||
flags := cmd.Flags()
|
||||
if flags == nil {
|
||||
return "", ErrHookTemplateParse
|
||||
}
|
||||
return c.cmd.Name()
|
||||
}
|
||||
|
||||
// flagValue returns the value that was set for the given flag when the hook was invoked.
|
||||
func (c commandInfo) flagValue(flagName string) (string, error) {
|
||||
if c.cmd == nil {
|
||||
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
|
||||
}
|
||||
f := c.cmd.Flag(flagName)
|
||||
if f == nil {
|
||||
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
|
||||
}
|
||||
return f.Value.String(), nil
|
||||
}
|
||||
|
||||
// argValue returns the value of the nth argument.
|
||||
func (c commandInfo) argValue(n int) (string, error) {
|
||||
if c.cmd == nil {
|
||||
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
|
||||
}
|
||||
flags := c.cmd.Flags()
|
||||
v := flags.Arg(n)
|
||||
if v == "" && n >= flags.NArg() {
|
||||
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
|
||||
}
|
||||
return v, nil
|
||||
return flags.Arg(i), nil
|
||||
}
|
||||
|
||||
@@ -1,67 +1,43 @@
|
||||
package hooks_test
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// TestParseTemplate tests parsing templates as returned by plugins.
|
||||
//
|
||||
// It uses fixed string fixtures to lock in compatibility with existing
|
||||
// plugin templates, so older formats continue to work even if we add new
|
||||
// template forms.
|
||||
//
|
||||
// For helper-backed cases, it also verifies that templates produced by the
|
||||
// current TemplateReplace* helpers parse to the same output. This lets us
|
||||
// evolve the emitted template format without breaking older plugins.
|
||||
func TestParseTemplate(t *testing.T) {
|
||||
type testFlag struct {
|
||||
name string
|
||||
value string
|
||||
}
|
||||
tests := []struct {
|
||||
doc string
|
||||
template string // compatibility fixture; keep even if helpers emit a newer form
|
||||
templateFunc func() string
|
||||
testCases := []struct {
|
||||
template string
|
||||
flags []testFlag
|
||||
args []string
|
||||
expectedOutput []string
|
||||
}{
|
||||
{
|
||||
doc: "empty template",
|
||||
template: "",
|
||||
expectedOutput: []string{""},
|
||||
},
|
||||
{
|
||||
doc: "plain message",
|
||||
template: "a plain template message",
|
||||
expectedOutput: []string{"a plain template message"},
|
||||
},
|
||||
{
|
||||
doc: "subcommand name",
|
||||
template: "hello {{.Name}}", // NOTE: fixture; do not modify without considering plugin compatibility
|
||||
templateFunc: func() string { return "hello " + hooks.TemplateReplaceSubcommandName() },
|
||||
|
||||
expectedOutput: []string{"hello pull"},
|
||||
},
|
||||
{
|
||||
doc: "single flag",
|
||||
template: `{{flag . "tag"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
||||
templateFunc: func() string { return hooks.TemplateReplaceFlagValue("tag") },
|
||||
template: TemplateReplaceFlagValue("tag"),
|
||||
flags: []testFlag{
|
||||
{name: "tag", value: "my-tag"},
|
||||
{
|
||||
name: "tag",
|
||||
value: "my-tag",
|
||||
},
|
||||
},
|
||||
expectedOutput: []string{"my-tag"},
|
||||
},
|
||||
{
|
||||
doc: "multiple flags",
|
||||
template: `{{flag . "test-one"}} {{flag . "test2"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
||||
templateFunc: func() string {
|
||||
return hooks.TemplateReplaceFlagValue("test-one") + " " + hooks.TemplateReplaceFlagValue("test2")
|
||||
},
|
||||
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
|
||||
flags: []testFlag{
|
||||
{
|
||||
name: "test-one",
|
||||
@@ -75,51 +51,36 @@ func TestParseTemplate(t *testing.T) {
|
||||
expectedOutput: []string{"value value2"},
|
||||
},
|
||||
{
|
||||
doc: "multiple args",
|
||||
template: `{{arg . 0}} {{arg . 1}}`, // NOTE: fixture; do not modify without considering plugin compatibility
|
||||
templateFunc: func() string { return hooks.TemplateReplaceArg(0) + " " + hooks.TemplateReplaceArg(1) },
|
||||
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
|
||||
args: []string{"zero", "one"},
|
||||
expectedOutput: []string{"zero one"},
|
||||
},
|
||||
{
|
||||
doc: "arg in sentence",
|
||||
template: "You just pulled {{arg . 0}}", // NOTE: fixture; do not modify without considering plugin compatibility
|
||||
templateFunc: func() string { return "You just pulled " + hooks.TemplateReplaceArg(0) },
|
||||
template: "You just pulled " + TemplateReplaceArg(0),
|
||||
args: []string{"alpine"},
|
||||
expectedOutput: []string{"You just pulled alpine"},
|
||||
},
|
||||
{
|
||||
doc: "multiline output",
|
||||
template: "one line\nanother line!",
|
||||
expectedOutput: []string{"one line", "another line!"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
testCmd := &cobra.Command{
|
||||
Use: "pull",
|
||||
Args: cobra.ExactArgs(len(tc.args)),
|
||||
}
|
||||
for _, f := range tc.flags {
|
||||
_ = testCmd.Flags().String(f.name, "", "")
|
||||
err := testCmd.Flag(f.name).Value.Set(f.value)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
err := testCmd.Flags().Parse(tc.args)
|
||||
for _, tc := range testCases {
|
||||
testCmd := &cobra.Command{
|
||||
Use: "pull",
|
||||
Args: cobra.ExactArgs(len(tc.args)),
|
||||
}
|
||||
for _, f := range tc.flags {
|
||||
_ = testCmd.Flags().String(f.name, "", "")
|
||||
err := testCmd.Flag(f.name).Value.Set(f.value)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
err := testCmd.Flags().Parse(tc.args)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Validate using fixtures.
|
||||
out, err := hooks.ParseTemplate(tc.template, testCmd)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
||||
|
||||
if tc.templateFunc != nil {
|
||||
// Validate using the current template function equivalent.
|
||||
out, err = hooks.ParseTemplate(tc.templateFunc(), testCmd)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
||||
}
|
||||
})
|
||||
out, err := ParseTemplate(tc.template, testCmd)
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, out, tc.expectedOutput)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
flags := rootCmd.PersistentFlags()
|
||||
flags.SetOutput(nil)
|
||||
if err := flags.Parse(args); err != nil {
|
||||
perr := flags.Parse(args)
|
||||
if perr != nil {
|
||||
return err
|
||||
}
|
||||
if flags.Changed("help") {
|
||||
@@ -59,11 +60,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things.
|
||||
cargs = append(cargs, args...)
|
||||
cargs = append(cargs, toComplete)
|
||||
origArgs := os.Args
|
||||
os.Args = cargs
|
||||
defer func() {
|
||||
os.Args = origArgs
|
||||
}()
|
||||
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
|
||||
if runErr != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
@@ -28,58 +24,3 @@ func TestPluginResourceAttributesEnvvar(t *testing.T) {
|
||||
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
|
||||
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
|
||||
}
|
||||
|
||||
func TestPluginStubRunEReturnsParseError(t *testing.T) {
|
||||
cmd, err := preparePluginStubCommand(t)
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = cmd.RunE(cmd, []string{"--definitely-not-a-real-flag"})
|
||||
assert.ErrorContains(t, err, "unknown flag: --definitely-not-a-real-flag")
|
||||
}
|
||||
|
||||
func TestPluginStubCompletionRestoresOSArgs(t *testing.T) {
|
||||
cmd, err := preparePluginStubCommand(t)
|
||||
assert.NilError(t, err)
|
||||
|
||||
savedArgs := os.Args
|
||||
t.Cleanup(func() { os.Args = savedArgs })
|
||||
|
||||
originalArgs := []string{"docker", "image", "ls"}
|
||||
os.Args = append([]string(nil), originalArgs...)
|
||||
|
||||
_, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp")
|
||||
assert.Equal(t, directive, cobra.ShellCompDirectiveError)
|
||||
assert.DeepEqual(t, os.Args, originalArgs)
|
||||
}
|
||||
|
||||
func preparePluginStubCommand(t *testing.T) (*cobra.Command, error) {
|
||||
t.Helper()
|
||||
pluginCommandStubsOnce = sync.Once{}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
const cliPlugin = `#!/bin/sh
|
||||
printf '%s' '{"SchemaVersion":"0.1.0"}'
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(tmpDir, "docker-testplugin"), []byte(cliPlugin), 0o777); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(nil)
|
||||
cli.ConfigFile().CLIPluginsExtraDirs = []string{tmpDir}
|
||||
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
root.PersistentFlags().Bool("debug", false, "")
|
||||
|
||||
if err := AddPluginCommandStubs(cli, root); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd, _, err := root.Find([]string{"testplugin"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmd == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package manager
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
@@ -22,11 +19,15 @@ import (
|
||||
// HookPluginData is the type representing the information
|
||||
// that plugins declaring support for hooks get passed when
|
||||
// being invoked following a CLI command execution.
|
||||
//
|
||||
// Deprecated: use [hooks.Request] instead.
|
||||
//
|
||||
//go:fix inline
|
||||
type HookPluginData = hooks.Request
|
||||
type HookPluginData struct {
|
||||
// RootCmd is a string representing the matching hook configuration
|
||||
// which is currently being invoked. If a hook for `docker context` is
|
||||
// configured and the user executes `docker context ls`, the plugin will
|
||||
// be invoked with `context`.
|
||||
RootCmd string
|
||||
Flags map[string]string
|
||||
CommandError string
|
||||
}
|
||||
|
||||
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
|
||||
// a main CLI command was executed. It calls the hook subcommand for all
|
||||
@@ -54,8 +55,11 @@ func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subComma
|
||||
}
|
||||
|
||||
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
|
||||
if ctx.Err() != nil {
|
||||
// check if the context was cancelled before invoking hooks
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
pluginsCfg := cfg.Plugins
|
||||
@@ -65,65 +69,47 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
|
||||
|
||||
pluginDirs := getPluginDirs(cfg)
|
||||
nextSteps := make([]string, 0, len(pluginsCfg))
|
||||
|
||||
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
|
||||
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !matched {
|
||||
return nil, false, nil
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
match, ok := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := p.RunHook(ctx, hooks.Request{
|
||||
hookReturn, err := p.RunHook(ctx, HookPluginData{
|
||||
RootCmd: match,
|
||||
Flags: flags,
|
||||
CommandError: cmdErrorMessage,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
continue
|
||||
}
|
||||
|
||||
var message hooks.Response
|
||||
if err := json.Unmarshal(resp, &message); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
|
||||
var hookMessageData hooks.HookMessage
|
||||
err = json.Unmarshal(hookReturn, &hookMessageData)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// currently the only hook type
|
||||
if message.Type != hooks.NextSteps {
|
||||
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
|
||||
}
|
||||
|
||||
messages, err = hooks.ParseTemplate(message.Template, subCmd)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
return messages, true, nil
|
||||
}
|
||||
|
||||
for pluginName, pluginCfg := range pluginsCfg {
|
||||
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
|
||||
if err != nil {
|
||||
// skip misbehaving plugins, but don't halt execution
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"error": err,
|
||||
"plugin": pluginName,
|
||||
}).Debug("Plugin hook invocation failed")
|
||||
if hookMessageData.Type != hooks.NextSteps {
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
|
||||
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var appended bool
|
||||
nextSteps, appended = appendNextSteps(nextSteps, messages)
|
||||
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
|
||||
if !appended {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"plugin": pluginName,
|
||||
}).Debug("Plugin responded with an empty hook message; ignoring")
|
||||
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
|
||||
}
|
||||
}
|
||||
return nextSteps
|
||||
|
||||
@@ -177,8 +177,9 @@ func TestGetPluginDirs(t *testing.T) {
|
||||
pluginDirs := getPluginDirs(cli.ConfigFile())
|
||||
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
|
||||
|
||||
extras := make([]string, 0, 3+len(expected))
|
||||
extras = append(extras, "foo", "bar", "baz")
|
||||
extras := []string{
|
||||
"foo", "bar", "baz",
|
||||
}
|
||||
expected = append(extras, expected...)
|
||||
cli.SetConfigFile(&configfile.ConfigFile{
|
||||
CLIPluginsExtraDirs: extras,
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -155,7 +154,7 @@ func validateSchemaVersion(version string) error {
|
||||
|
||||
// RunHook executes the plugin's hooks command
|
||||
// and returns its unprocessed output.
|
||||
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, error) {
|
||||
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
|
||||
hDataBytes, err := json.Marshal(hookData)
|
||||
if err != nil {
|
||||
return nil, wrapAsPluginError(err, "failed to marshall hook data")
|
||||
@@ -164,16 +163,12 @@ func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, e
|
||||
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
|
||||
pCmd.Env = os.Environ()
|
||||
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
|
||||
|
||||
out, err := pCmd.Output()
|
||||
hookCmdOutput, err := pCmd.Output()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return nil, wrapAsPluginError(err, "plugin hook subcommand exited unsuccessfully")
|
||||
}
|
||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand: "+pCmd.String())
|
||||
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
|
||||
}
|
||||
return out, nil
|
||||
|
||||
return hookCmdOutput, nil
|
||||
}
|
||||
|
||||
// pluginNameFormat is used as part of errors for invalid plugin-names.
|
||||
|
||||
@@ -54,9 +54,9 @@ func TestPluginServer(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("allows reconnects", func(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
var calls int32
|
||||
h := func(_ net.Conn) {
|
||||
calls.Add(1)
|
||||
atomic.AddInt32(&calls, 1)
|
||||
}
|
||||
|
||||
srv, err := NewPluginServer(h)
|
||||
@@ -70,7 +70,7 @@ func TestPluginServer(t *testing.T) {
|
||||
|
||||
waitForCalls := func(n int) {
|
||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
||||
if calls.Load() == int32(n) {
|
||||
if atomic.LoadInt32(&calls) == int32(n) {
|
||||
return poll.Success()
|
||||
}
|
||||
return poll.Continue("waiting for handler to be called")
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package command
|
||||
|
||||
@@ -566,12 +566,11 @@ type ServerInfo struct {
|
||||
// It applies by default the standard streams, and the content trust from
|
||||
// environment.
|
||||
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||
defaultOps := make([]CLIOption, 0, 3+len(ops))
|
||||
defaultOps = append(defaultOps,
|
||||
defaultOps := []CLIOption{
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
)
|
||||
}
|
||||
ops = append(defaultOps, ops...)
|
||||
|
||||
cli := &DockerCli{baseCtx: context.Background()}
|
||||
|
||||
+15
-33
@@ -208,70 +208,52 @@ func TestInitializeFromClient(t *testing.T) {
|
||||
// Makes sure we don't hang forever on the initial connection.
|
||||
// https://github.com/docker/cli/issues/3652
|
||||
func TestInitializeFromClientHangs(t *testing.T) {
|
||||
const (
|
||||
// Sized against measured scheduler stalls:
|
||||
// under CPU pressure this test sees 40-90ms stalls;
|
||||
// this should give about 5x headroom.
|
||||
// See https://github.com/docker/cli/issues/6003.
|
||||
clientInitTimeout = 500 * time.Millisecond
|
||||
|
||||
// This is only a backstop against a genuine hang.
|
||||
// It should never be reached on a healthy run.
|
||||
// So, it should be fine to have a lenient timeout here.
|
||||
waitTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
socket := filepath.Join(tmpDir, "my.sock")
|
||||
l, err := net.Listen("unix", socket)
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Buffered, so the handler can record
|
||||
// that it was reached without a reader
|
||||
// having to be ready at that instant.
|
||||
receivedReqCh := make(chan struct{}, 1)
|
||||
releaseHandlerCh := make(chan struct{})
|
||||
receiveReqCh := make(chan bool)
|
||||
timeoutCtx, cancel := context.WithTimeout(context.TODO(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Simulate a server that hangs on connections.
|
||||
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
select {
|
||||
case receivedReqCh <- struct{}{}:
|
||||
default:
|
||||
case <-timeoutCtx.Done():
|
||||
case receiveReqCh <- true: // Blocks until someone receives on the channel.
|
||||
}
|
||||
<-releaseHandlerCh
|
||||
_, _ = w.Write([]byte("OK"))
|
||||
}))
|
||||
ts.Listener = l
|
||||
ts.Start()
|
||||
t.Cleanup(func() {
|
||||
close(releaseHandlerCh)
|
||||
ts.Close()
|
||||
})
|
||||
defer ts.Close()
|
||||
|
||||
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}}
|
||||
configFile := &configfile.ConfigFile{}
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
|
||||
initErrCh := make(chan error, 1)
|
||||
initializedCh := make(chan bool)
|
||||
|
||||
go func() {
|
||||
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout}
|
||||
cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
|
||||
err := cli.Initialize(flags.NewClientOptions())
|
||||
assert.Check(t, err)
|
||||
cli.CurrentVersion()
|
||||
initErrCh <- err
|
||||
close(initializedCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-initErrCh:
|
||||
assert.Check(t, err)
|
||||
case <-time.After(waitTimeout):
|
||||
case <-timeoutCtx.Done():
|
||||
t.Fatal("timeout waiting for initialization to complete")
|
||||
case <-initializedCh:
|
||||
}
|
||||
|
||||
select {
|
||||
case <-receivedReqCh:
|
||||
case <-time.After(waitTimeout):
|
||||
case <-timeoutCtx.Done():
|
||||
t.Fatal("server never received an init request")
|
||||
case <-receiveReqCh:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -22,7 +23,7 @@ type APIClientProvider interface {
|
||||
|
||||
// ImageNames offers completion for images present within the local store
|
||||
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
@@ -35,14 +36,14 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
names = append(names, img.RepoTags...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ImageNamesWithBase offers completion for images present within the local store,
|
||||
// including both full image names with tags and base image names (repository names only)
|
||||
// when multiple tags exist for the same base name
|
||||
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
|
||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
@@ -68,14 +69,14 @@ func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.Completion
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerNames offers completion for container names and IDs
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
|
||||
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
|
||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
All: all,
|
||||
})
|
||||
@@ -100,21 +101,15 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
if showContainerIDs {
|
||||
names = append(names, ctr.ID)
|
||||
}
|
||||
for _, n := range ctr.Names {
|
||||
// Skip legacy link names: "/linked-container/link-name"
|
||||
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
|
||||
continue
|
||||
}
|
||||
names = append(names, strings.TrimPrefix(n, "/"))
|
||||
}
|
||||
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// VolumeNames offers completion for volumes
|
||||
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
@@ -124,12 +119,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
names = append(names, vol.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkNames offers completion for networks
|
||||
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
@@ -139,7 +134,7 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
names = append(names, nw.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarNames offers completion for environment-variable names. This
|
||||
@@ -156,7 +151,7 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
// docker run --rm --env MY_VAR alpine printenv MY_VAR
|
||||
// hello
|
||||
func EnvVarNames() cobra.CompletionFunc {
|
||||
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
@@ -164,34 +159,12 @@ func EnvVarNames() cobra.CompletionFunc {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FromList offers completion for the given list of options.
|
||||
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
|
||||
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
|
||||
}
|
||||
|
||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||
@@ -245,38 +218,3 @@ func Platforms() cobra.CompletionFunc {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// Unique wraps a completion func and removes completion results that are
|
||||
// already consumed (i.e., appear in "args").
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// # initial completion: args is empty, so all results are shown
|
||||
// command <tab>
|
||||
// one two three
|
||||
//
|
||||
// # "one" is already used so omitted
|
||||
// command one <tab>
|
||||
// two three
|
||||
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
all, dir := fn(cmd, args, toComplete)
|
||||
if len(all) == 0 || len(args) == 0 {
|
||||
return all, dir
|
||||
}
|
||||
|
||||
alreadyCompleted := make(map[string]struct{}, len(args))
|
||||
for _, a := range args {
|
||||
alreadyCompleted[a] = struct{}{}
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(all))
|
||||
for _, c := range all {
|
||||
if _, ok := alreadyCompleted[c]; !ok {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
|
||||
return out, dir
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-c", "container-b", "container-a"},
|
||||
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
@@ -97,7 +97,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
|
||||
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
@@ -107,7 +107,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
},
|
||||
expOut: []string{"container-c"},
|
||||
expOut: []string{"container-c", "container-c/link-b"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@@ -117,7 +117,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
@@ -133,7 +133,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
|
||||
},
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
|
||||
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
|
||||
},
|
||||
@@ -196,18 +196,6 @@ func TestCompleteFromList(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(values, expected))
|
||||
}
|
||||
|
||||
func TestWithPrefix(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
|
||||
}
|
||||
|
||||
func TestWithSuffix(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
|
||||
}
|
||||
|
||||
func TestCompleteImageNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
@@ -363,23 +351,3 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnique(t *testing.T) {
|
||||
base := []string{"alpha", "beta", "gamma"}
|
||||
|
||||
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
|
||||
return base, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
|
||||
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
|
||||
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
|
||||
|
||||
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
|
||||
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
|
||||
|
||||
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -106,7 +102,6 @@ func (c *configContext) Labels() string {
|
||||
for k, v := range mapLabels {
|
||||
joinLabels = append(joinLabels, k+"="+v)
|
||||
}
|
||||
slices.Sort(joinLabels)
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package config
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ type fakeClient struct {
|
||||
infoFunc func() (client.SystemInfoResult, error)
|
||||
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
|
||||
containerCopyToFunc func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error)
|
||||
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
|
||||
waitFunc func(string) client.ContainerWaitResult
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
@@ -129,13 +128,6 @@ func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, op
|
||||
return client.CopyFromContainerResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyToContainer(_ context.Context, containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
||||
if f.containerCopyToFunc != nil {
|
||||
return f.containerCopyToFunc(containerID, options)
|
||||
}
|
||||
return client.CopyToContainerResult{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(containerID, options)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
@@ -182,35 +182,6 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// completeLinks implements shell completion for the `--link` option of `rm --link`.
|
||||
//
|
||||
// It contacts the API to get names of legacy links on containers.
|
||||
// In case of an error, an empty list is returned.
|
||||
func completeLinks(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, ctr := range res.Items {
|
||||
if len(ctr.Names) <= 1 {
|
||||
// Container has no links names.
|
||||
continue
|
||||
}
|
||||
for _, n := range ctr.Names {
|
||||
// Skip legacy link names: "/linked-container/link-name"
|
||||
if len(n) > 1 && strings.IndexByte(n[1:], '/') != -1 {
|
||||
names = append(names, strings.TrimPrefix(n, "/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
|
||||
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
|
||||
// of the build-in log drivers.
|
||||
|
||||
@@ -135,44 +135,3 @@ func TestCompleteSignals(t *testing.T) {
|
||||
assert.Check(t, len(values) > 1)
|
||||
assert.Check(t, is.Len(values, len(signal.SignalMap)))
|
||||
}
|
||||
|
||||
func TestCompleteLinks(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
showAll, showIDs bool
|
||||
filters []func(container.Summary) bool
|
||||
containers []container.Summary
|
||||
expOut []string
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no results",
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "all containers",
|
||||
showAll: true,
|
||||
containers: []container.Summary{
|
||||
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b", "/container-c/link-c"}},
|
||||
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b", "/container-b/link-a"}},
|
||||
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
|
||||
},
|
||||
expOut: []string{"container-c/link-b", "container-c/link-c", "container-b/link-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := completeLinks(test.NewFakeCli(&fakeClient{
|
||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{Items: tc.containers}, nil
|
||||
},
|
||||
}))
|
||||
|
||||
containers, directives := comp(&cobra.Command{}, nil, "")
|
||||
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
|
||||
assert.Check(t, is.DeepEqual(containers, tc.expOut))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ container source to stdout.`,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symlinks in SRC_PATH")
|
||||
flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH")
|
||||
flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)")
|
||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached")
|
||||
return cmd
|
||||
@@ -168,50 +168,6 @@ func progressHumanSize(n int64) string {
|
||||
return units.HumanSizeWithPrecision(float64(n), 3)
|
||||
}
|
||||
|
||||
// localContentSize returns the total size of regular file content at path.
|
||||
// For a regular file it returns the file size. For a directory it walks
|
||||
// the tree and sums sizes of all regular files.
|
||||
func localContentSize(path string) (int64, error) {
|
||||
fi, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
if fi.Mode().IsRegular() {
|
||||
return fi.Size(), nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
var total int64
|
||||
err = filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Type().IsRegular() {
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
total += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return total, err
|
||||
}
|
||||
|
||||
// copySummary formats the "Successfully copied ..." message.
|
||||
// When contentSize differs from transferredSize, both values are shown.
|
||||
func copySummary(contentSize, transferredSize int64, dest string) string {
|
||||
if contentSize != transferredSize {
|
||||
return fmt.Sprintf("Successfully copied %s (transferred %s) to %s\n",
|
||||
progressHumanSize(contentSize), progressHumanSize(transferredSize), dest,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf("Successfully copied %s to %s\n",
|
||||
progressHumanSize(contentSize), dest,
|
||||
)
|
||||
}
|
||||
|
||||
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
|
||||
srcContainer, srcPath := splitCpArg(opts.source)
|
||||
destContainer, destPath := splitCpArg(opts.destination)
|
||||
@@ -271,7 +227,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
// if client requests to follow symlinks, then must decide target file to be copied
|
||||
// if client requests to follow symbol link, then must decide target file to be copied
|
||||
var rebaseName string
|
||||
if copyConfig.followLink {
|
||||
src, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{
|
||||
@@ -339,11 +295,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
cancel()
|
||||
<-done
|
||||
restore()
|
||||
reportedSize := copiedSize
|
||||
if !cpRes.Stat.Mode.IsDir() {
|
||||
reportedSize = cpRes.Stat.Size
|
||||
}
|
||||
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, dstPath))
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -402,14 +354,11 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
content io.ReadCloser
|
||||
resolvedDstPath string
|
||||
copiedSize int64
|
||||
contentSize int64
|
||||
sizeErr error
|
||||
)
|
||||
|
||||
if srcPath == "-" {
|
||||
content = os.Stdin
|
||||
resolvedDstPath = dstInfo.Path
|
||||
sizeErr = errors.New("content size not available for stdin")
|
||||
if !dstInfo.IsDir {
|
||||
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
|
||||
}
|
||||
@@ -420,8 +369,6 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
return err
|
||||
}
|
||||
|
||||
contentSize, sizeErr = localContentSize(srcInfo.Path)
|
||||
|
||||
srcArchive, err := archive.TarResource(srcInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -474,11 +421,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
cancel()
|
||||
<-done
|
||||
restore()
|
||||
reportedSize := copiedSize
|
||||
if sizeErr == nil {
|
||||
reportedSize = contentSize
|
||||
}
|
||||
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, copyConfig.container+":"+dstInfo.Path))
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/go-archive/compression"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -212,237 +211,3 @@ func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
|
||||
expected := `"/dev/random" must be a directory or a regular file`
|
||||
assert.ErrorContains(t, err, expected)
|
||||
}
|
||||
|
||||
func TestCopySummary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentSize int64
|
||||
transferredSize int64
|
||||
dest string
|
||||
wantContains string
|
||||
wantNoContain string
|
||||
}{
|
||||
{
|
||||
name: "different sizes shows both",
|
||||
contentSize: 5,
|
||||
transferredSize: 2048,
|
||||
dest: "/dst",
|
||||
wantContains: "(transferred",
|
||||
},
|
||||
{
|
||||
name: "equal sizes shows single value",
|
||||
contentSize: 100,
|
||||
transferredSize: 100,
|
||||
dest: "/dst",
|
||||
wantNoContain: "(transferred",
|
||||
},
|
||||
{
|
||||
name: "both zero",
|
||||
contentSize: 0,
|
||||
transferredSize: 0,
|
||||
dest: "ctr:/dst",
|
||||
wantContains: "Successfully copied 0B to ctr:/dst",
|
||||
wantNoContain: "(transferred",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := copySummary(tc.contentSize, tc.transferredSize, tc.dest)
|
||||
if tc.wantContains != "" {
|
||||
assert.Check(t, is.Contains(got, tc.wantContains))
|
||||
}
|
||||
if tc.wantNoContain != "" {
|
||||
assert.Check(t, !strings.Contains(got, tc.wantNoContain), "unexpected substring %q in %q", tc.wantNoContain, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFromContainerReportsFileSize(t *testing.T) {
|
||||
// The file content is "hello" (5 bytes), but the TAR archive wrapping
|
||||
// it is much larger due to headers and padding. The success message
|
||||
// should report the actual file size (5B), not the TAR stream size.
|
||||
srcDir := fs.NewDir(t, "cp-test-from",
|
||||
fs.WithFile("file1", "hello"))
|
||||
|
||||
destDir := fs.NewDir(t, "cp-test-from-dest")
|
||||
|
||||
const fileSize int64 = 5
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
Stat: container.PathStat{
|
||||
Name: "file1",
|
||||
Size: fileSize,
|
||||
},
|
||||
}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: "container:/file1",
|
||||
destination: destDir.Path(),
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied 5B"))
|
||||
assert.Check(t, is.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
func TestCopyToContainerReportsFileSize(t *testing.T) {
|
||||
// Create a temp file with known content ("hello" = 5 bytes).
|
||||
// The TAR archive sent to the container is larger, but the success
|
||||
// message should report the actual content size.
|
||||
srcFile := fs.NewFile(t, "cp-test-to", fs.WithContent("hello"))
|
||||
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
|
||||
return client.ContainerStatPathResult{
|
||||
Stat: container.PathStat{
|
||||
Name: "tmp",
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
||||
_, _ = io.Copy(io.Discard, options.Content)
|
||||
return client.CopyToContainerResult{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: srcFile.Path(),
|
||||
destination: "container:/tmp",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied 5B"))
|
||||
assert.Check(t, is.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
func TestCopyToContainerReportsEmptyFileSize(t *testing.T) {
|
||||
srcFile := fs.NewFile(t, "cp-test-empty", fs.WithContent(""))
|
||||
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
|
||||
return client.ContainerStatPathResult{
|
||||
Stat: container.PathStat{
|
||||
Name: "tmp",
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
||||
_, _ = io.Copy(io.Discard, options.Content)
|
||||
return client.CopyToContainerResult{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: srcFile.Path(),
|
||||
destination: "container:/tmp",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied 0B"))
|
||||
assert.Check(t, is.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
func TestCopyToContainerReportsDirectorySize(t *testing.T) {
|
||||
// Create a temp directory with files "aaa" (3 bytes) + "bbb" (3 bytes) = 6 bytes.
|
||||
// The TAR archive is much larger, but the success message should report 6B.
|
||||
srcDir := fs.NewDir(t, "cp-test-dir",
|
||||
fs.WithFile("aaa", "aaa"),
|
||||
fs.WithFile("bbb", "bbb"),
|
||||
)
|
||||
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
|
||||
return client.ContainerStatPathResult{
|
||||
Stat: container.PathStat{
|
||||
Name: "tmp",
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
||||
_, _ = io.Copy(io.Discard, options.Content)
|
||||
return client.CopyToContainerResult{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: srcDir.Path() + string(os.PathSeparator),
|
||||
destination: "container:/tmp",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied 6B"))
|
||||
assert.Check(t, is.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
func TestCopyFromContainerReportsDirectorySize(t *testing.T) {
|
||||
// When copying a directory from a container, cpRes.Stat.Mode.IsDir() is true,
|
||||
// so reportedSize falls back to copiedSize (the tar stream bytes).
|
||||
srcDir := fs.NewDir(t, "cp-test-fromdir",
|
||||
fs.WithFile("file1", "hello"))
|
||||
|
||||
destDir := fs.NewDir(t, "cp-test-fromdir-dest")
|
||||
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
Stat: container.PathStat{
|
||||
Name: "mydir",
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: "container:/mydir",
|
||||
destination: destDir.Path(),
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied"))
|
||||
// For directories from container, content size is unknown so
|
||||
// reportedSize == copiedSize and "(transferred ...)" is omitted.
|
||||
assert.Check(t, !strings.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
func TestCopyToContainerStdinReportsTransferredSize(t *testing.T) {
|
||||
// When copying from stdin, content size is unknown.
|
||||
// The message should report transferred bytes without "(transferred ...)".
|
||||
r, w, _ := os.Pipe()
|
||||
_, _ = w.WriteString("some data from stdin")
|
||||
w.Close()
|
||||
oldStdin := os.Stdin
|
||||
os.Stdin = r
|
||||
t.Cleanup(func() { os.Stdin = oldStdin })
|
||||
|
||||
fakeCli := test.NewFakeCli(&fakeClient{
|
||||
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
|
||||
return client.ContainerStatPathResult{
|
||||
Stat: container.PathStat{
|
||||
Name: "tmp",
|
||||
Mode: os.ModeDir | 0o755,
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
|
||||
_, _ = io.Copy(io.Discard, options.Content)
|
||||
return client.CopyToContainerResult{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), fakeCli, copyOptions{
|
||||
source: "-",
|
||||
destination: "container:/tmp",
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
errOut := fakeCli.ErrBuffer().String()
|
||||
assert.Check(t, is.Contains(errOut, "Successfully copied"))
|
||||
// stdin has no content size, so reportedSize == copiedSize and
|
||||
// "(transferred ...)" should not appear.
|
||||
assert.Check(t, !strings.Contains(errOut, "(transferred"))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
@@ -112,27 +111,31 @@ func NewStatsFormat(source, osType string) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// NewStats returns a new Stats entity using the given ID, ID-prefix, or
|
||||
// name to resolve the container.
|
||||
func NewStats(idOrName string) *Stats {
|
||||
// FIXME(thaJeztah): "idOrName" is used for fuzzy-matching the container, which can result in multiple stats for the same container.
|
||||
// We should resolve the canonical ID once, then use that as reference
|
||||
// to prevent duplicates. Various parts in the code compare Container
|
||||
// against "ID" only (not considering "name" or "ID-prefix").
|
||||
return &Stats{StatsEntry: StatsEntry{Container: idOrName}}
|
||||
// NewStats returns a new Stats entity and sets in it the given name
|
||||
func NewStats(container string) *Stats {
|
||||
return &Stats{StatsEntry: StatsEntry{Container: container}}
|
||||
}
|
||||
|
||||
// statsFormatWrite renders the context for a list of containers statistics
|
||||
func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string, trunc bool) error {
|
||||
// TODO(thaJeztah): this should be taken from the (first) StatsEntry instead.
|
||||
// also, assuming all stats are for the same platform (and basing the
|
||||
// column headers on that) won't allow aggregated results, which could
|
||||
// be mixed platform.
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cstats := range stats {
|
||||
statsCtx := &statsContext{
|
||||
s: cstats,
|
||||
os: osType,
|
||||
trunc: trunc,
|
||||
}
|
||||
if err := format(statsCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
memUsage := memUseHeader
|
||||
if osType == winOSType {
|
||||
memUsage = winMemUseHeader
|
||||
}
|
||||
statsCtx := statsContext{os: osType}
|
||||
statsCtx := statsContext{}
|
||||
statsCtx.Header = formatter.SubHeaderContext{
|
||||
"Container": containerHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
@@ -144,18 +147,8 @@ func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string,
|
||||
"BlockIO": blockIOHeader,
|
||||
"PIDs": pidsHeader,
|
||||
}
|
||||
return ctx.Write(&statsCtx, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cstats := range stats {
|
||||
if err := format(&statsContext{
|
||||
s: cstats,
|
||||
os: osType,
|
||||
trunc: trunc,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
statsCtx.os = osType
|
||||
return ctx.Write(&statsCtx, render)
|
||||
}
|
||||
|
||||
type statsContext struct {
|
||||
@@ -174,9 +167,9 @@ func (c *statsContext) Container() string {
|
||||
}
|
||||
|
||||
func (c *statsContext) Name() string {
|
||||
// Trim the "/" prefix (if present).
|
||||
if name := strings.TrimPrefix(c.s.Name, "/"); name != "" {
|
||||
return name
|
||||
// TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char.
|
||||
if len(c.s.Name) > 1 {
|
||||
return c.s.Name[1:]
|
||||
}
|
||||
return noValue
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
@@ -371,7 +371,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
var binds []string
|
||||
volumes := copts.volumes.GetMap()
|
||||
// add any bind targets to the list of container volumes
|
||||
for bind := range volumes {
|
||||
for bind := range copts.volumes.GetMap() {
|
||||
parsed, err := volumespec.Parse(bind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -506,13 +506,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
|
||||
// collect all the environment variables for the container
|
||||
envVariables, err := opts.ReadKVEnvStrings(copts.envFile.GetSlice(), copts.env.GetSlice())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("--env-file: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// collect all the labels for the container
|
||||
labels, err := opts.ReadKVStrings(copts.labelsFile.GetSlice(), copts.labels.GetSlice())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("--label-file: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pidMode := container.PidMode(copts.pidMode)
|
||||
|
||||
@@ -829,6 +829,13 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||
Name: container.RestartPolicyAlways,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "always:1",
|
||||
expected: container.RestartPolicy{
|
||||
Name: container.RestartPolicyAlways,
|
||||
MaximumRetryCount: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: "always:2:3",
|
||||
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
|
||||
@@ -854,16 +861,6 @@ func TestParseRestartPolicy(t *testing.T) {
|
||||
input: "unless-stopped:invalid",
|
||||
expectedErr: "invalid restart policy format: maximum retry count must be an integer",
|
||||
},
|
||||
|
||||
// Unknown / invalid combinations: validation is handled by the daemon>
|
||||
{
|
||||
input: "anything:123",
|
||||
expected: container.RestartPolicy{Name: "anything", MaximumRetryCount: 123},
|
||||
},
|
||||
{
|
||||
input: "negative:-123",
|
||||
expected: container.RestartPolicy{Name: "negative", MaximumRetryCount: -123},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
@@ -940,13 +937,13 @@ func TestParseLoggingOpts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseEnvfileVariables(t *testing.T) {
|
||||
expErr := "--env-file: open nonexistent: no such file or directory"
|
||||
e := "open nonexistent: no such file or directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
expErr = "--env-file: open nonexistent: The system cannot find the file specified."
|
||||
e = "open nonexistent: The system cannot find the file specified."
|
||||
}
|
||||
// env ko
|
||||
if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != expErr {
|
||||
t.Fatalf("Expected an error with message '%s', got %v", expErr, err)
|
||||
if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
|
||||
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
||||
}
|
||||
// env ok
|
||||
config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"})
|
||||
@@ -993,13 +990,13 @@ func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseLabelfileVariables(t *testing.T) {
|
||||
expErr := "--label-file: open nonexistent: no such file or directory"
|
||||
e := "open nonexistent: no such file or directory"
|
||||
if runtime.GOOS == "windows" {
|
||||
expErr = "--label-file: open nonexistent: The system cannot find the file specified."
|
||||
e = "open nonexistent: The system cannot find the file specified."
|
||||
}
|
||||
// label ko
|
||||
if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != expErr {
|
||||
t.Fatalf("Expected an error with message '%s', got %v", expErr, err)
|
||||
if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e {
|
||||
t.Fatalf("Expected an error with message '%s', got %v", e, err)
|
||||
}
|
||||
// label ok
|
||||
config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"})
|
||||
|
||||
@@ -27,11 +27,6 @@ type rmOptions struct {
|
||||
func newRmCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts rmOptions
|
||||
|
||||
completeLinkNames := completeLinks(dockerCLI)
|
||||
completeNames := completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool {
|
||||
return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated
|
||||
})
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS] CONTAINER [CONTAINER...]",
|
||||
Short: "Remove one or more containers",
|
||||
@@ -43,13 +38,9 @@ func newRmCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container rm, docker container remove, docker rm",
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if opts.rmLink {
|
||||
// "--link" (remove link) is set; provide link names instead of container (primary) names.
|
||||
return completeLinkNames(cmd, args, toComplete)
|
||||
}
|
||||
return completeNames(cmd, args, toComplete)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool {
|
||||
return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated
|
||||
}),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
@@ -88,6 +79,7 @@ func runRm(ctx context.Context, dockerCLI command.Cli, opts *rmOptions) error {
|
||||
for _, name := range opts.containers {
|
||||
if err := <-errChan; err != nil {
|
||||
if opts.force && errdefs.IsNotFound(err) {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
|
||||
continue
|
||||
}
|
||||
errs = append(errs, err)
|
||||
|
||||
@@ -52,7 +52,6 @@ func TestRemoveForce(t *testing.T) {
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
assert.Equal(t, cli.ErrBuffer().String(), "")
|
||||
sort.Strings(removed)
|
||||
assert.DeepEqual(t, removed, []string{"mycontainer", "nosuchcontainer"})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -142,32 +144,39 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
|
||||
eh := newEventHandler()
|
||||
addEvents := []events.Action{events.ActionStart}
|
||||
if options.All {
|
||||
addEvents = append(addEvents, events.ActionCreate)
|
||||
eh.setHandler(events.ActionCreate, func(e events.Message) {
|
||||
if s := NewStats(e.Actor.ID); cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"event": e.Action,
|
||||
"container": e.Actor.ID,
|
||||
}).Debug("collecting stats for container")
|
||||
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
||||
}
|
||||
})
|
||||
}
|
||||
eh.setHandler(addEvents, func(ctx context.Context, e events.Message) {
|
||||
|
||||
eh.setHandler(events.ActionStart, func(e events.Message) {
|
||||
if s := NewStats(e.Actor.ID); cStats.add(s) {
|
||||
waitFirst.Add(1)
|
||||
log.G(ctx).Debug("collecting stats for container")
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"event": e.Action,
|
||||
"container": e.Actor.ID,
|
||||
}).Debug("collecting stats for container")
|
||||
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
|
||||
}
|
||||
})
|
||||
|
||||
// Remove containers when they are removed ("destroyed"); containers
|
||||
// do not emit [events.ActionRemove], only [events.ActionDestroy].
|
||||
//
|
||||
// When running with "--all" we don't remove containers when they die,
|
||||
// because they may come back, but without "--all" we remove them
|
||||
// on the first possible occasion (either "die" or "destroy").
|
||||
rmEvents := []events.Action{events.ActionDestroy}
|
||||
if !options.All {
|
||||
rmEvents = append(rmEvents, events.ActionDie)
|
||||
eh.setHandler(events.ActionDie, func(e events.Message) {
|
||||
log.G(ctx).WithFields(log.Fields{
|
||||
"event": e.Action,
|
||||
"container": e.Actor.ID,
|
||||
}).Debug("stop collecting stats for container")
|
||||
cStats.remove(e.Actor.ID)
|
||||
})
|
||||
}
|
||||
eh.setHandler(rmEvents, func(ctx context.Context, e events.Message) {
|
||||
log.G(ctx).Debug("stop collecting stats for container")
|
||||
cStats.remove(e.Actor.ID)
|
||||
})
|
||||
|
||||
// monitorContainerEvents watches for container creation and removal (only
|
||||
// used when calling `docker stats` without arguments).
|
||||
@@ -207,7 +216,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
|
||||
eventChan := make(chan events.Message)
|
||||
go eh.watch(ctx, eventChan)
|
||||
go eh.watch(eventChan)
|
||||
stopped := make(chan struct{})
|
||||
go monitorContainerEvents(started, eventChan, stopped)
|
||||
defer close(stopped)
|
||||
@@ -285,32 +294,30 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
}
|
||||
}
|
||||
|
||||
// renderBuf holds the formatted stats output produced by statsFormatWrite.
|
||||
// It does not include any terminal control sequences.
|
||||
var renderBuf bytes.Buffer
|
||||
|
||||
// frameBuf holds the final terminal frame, including cursor movement and
|
||||
// line-clearing escape sequences, written in a single pass to avoid flicker.
|
||||
var frameBuf bytes.Buffer
|
||||
// Buffer to store formatted stats text.
|
||||
// Once formatted, it will be printed in one write to avoid screen flickering.
|
||||
var statsTextBuffer bytes.Buffer
|
||||
|
||||
statsCtx := formatter.Context{
|
||||
Output: &renderBuf,
|
||||
Output: &statsTextBuffer,
|
||||
Format: NewStatsFormat(format, daemonOSType),
|
||||
}
|
||||
|
||||
if options.NoStream {
|
||||
statsList := cStats.snapshot()
|
||||
if len(statsList) == 0 {
|
||||
return nil
|
||||
}
|
||||
ccStats := make([]StatsEntry, 0, len(statsList))
|
||||
for _, c := range statsList {
|
||||
cStats.mu.RLock()
|
||||
ccStats := make([]StatsEntry, 0, len(cStats.cs))
|
||||
for _, c := range cStats.cs {
|
||||
ccStats = append(ccStats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.RUnlock()
|
||||
|
||||
if len(ccStats) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = dockerCLI.Out().Write(renderBuf.Bytes())
|
||||
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -319,38 +326,34 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
renderBuf.Reset()
|
||||
frameBuf.Reset()
|
||||
statsList := cStats.snapshot()
|
||||
if len(statsList) == 0 && !showAll {
|
||||
// Clear screen
|
||||
_, _ = io.WriteString(dockerCLI.Out(), "\033[H\033[J")
|
||||
return nil
|
||||
}
|
||||
ccStats := make([]StatsEntry, 0, len(statsList))
|
||||
for _, c := range statsList {
|
||||
cStats.mu.RLock()
|
||||
ccStats := make([]StatsEntry, 0, len(cStats.cs))
|
||||
for _, c := range cStats.cs {
|
||||
ccStats = append(ccStats, c.GetStatistics())
|
||||
}
|
||||
cStats.mu.RUnlock()
|
||||
|
||||
// Start by moving the cursor to the top-left
|
||||
_, _ = fmt.Fprint(&statsTextBuffer, "\033[H")
|
||||
|
||||
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start by moving the cursor to the top-left
|
||||
_, _ = io.WriteString(&frameBuf, "\033[H")
|
||||
|
||||
// TODO(thaJeztah): consider wrapping the writer to inject ANSI (line-clearing) during formatting.
|
||||
// instead of post-processing the results.
|
||||
for line := range bytes.SplitSeq(renderBuf.Bytes(), []byte{'\n'}) {
|
||||
for line := range strings.SplitSeq(statsTextBuffer.String(), "\n") {
|
||||
// In case the new text is shorter than the one we are writing over,
|
||||
// we'll append the "erase line" escape sequence to clear the remaining text.
|
||||
_, _ = frameBuf.Write(line)
|
||||
_, _ = io.WriteString(&frameBuf, "\033[K")
|
||||
_ = frameBuf.WriteByte('\n')
|
||||
_, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K")
|
||||
}
|
||||
// We might have fewer containers than before, so let's clear the remaining text
|
||||
_, _ = io.WriteString(&frameBuf, "\033[J")
|
||||
_, _ = dockerCLI.Out().Write(frameBuf.Bytes())
|
||||
_, _ = fmt.Fprint(&statsTextBuffer, "\033[J")
|
||||
|
||||
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
|
||||
statsTextBuffer.Reset()
|
||||
|
||||
if len(ccStats) == 0 && !showAll {
|
||||
return nil
|
||||
}
|
||||
case err, ok := <-closeChan:
|
||||
if !ok || err == nil || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
// Suppress "unexpected EOF" errors in the CLI so that
|
||||
@@ -366,38 +369,33 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
|
||||
|
||||
// newEventHandler initializes and returns an eventHandler
|
||||
func newEventHandler() *eventHandler {
|
||||
return &eventHandler{handlers: make(map[events.Action]func(context.Context, events.Message))}
|
||||
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
|
||||
}
|
||||
|
||||
// eventHandler allows for registering specific events to setHandler.
|
||||
type eventHandler struct {
|
||||
handlers map[events.Action]func(context.Context, events.Message)
|
||||
handlers map[events.Action]func(events.Message)
|
||||
}
|
||||
|
||||
func (eh *eventHandler) setHandler(actions []events.Action, handler func(context.Context, events.Message)) {
|
||||
for _, action := range actions {
|
||||
eh.handlers[action] = handler
|
||||
}
|
||||
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
|
||||
eh.handlers[action] = handler
|
||||
}
|
||||
|
||||
// watch ranges over the passed in event chan and processes the events based on the
|
||||
// handlers created for a given action.
|
||||
// To stop watching, close the event chan.
|
||||
func (eh *eventHandler) watch(ctx context.Context, c <-chan events.Message) {
|
||||
func (eh *eventHandler) watch(c <-chan events.Message) {
|
||||
for e := range c {
|
||||
h, exists := eh.handlers[e.Action]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if e.Actor.ID == "" {
|
||||
log.G(ctx).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action)
|
||||
log.G(context.TODO()).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action)
|
||||
continue
|
||||
}
|
||||
logger := log.G(ctx).WithFields(log.Fields{
|
||||
"event": e.Action,
|
||||
"container": e.Actor.ID,
|
||||
})
|
||||
|
||||
go h(log.WithLogger(ctx, logger), e)
|
||||
log.G(context.TODO()).WithField("event", e).Debugf("event handler: received %s event for: %s", e.Action, e.Actor.ID)
|
||||
go h(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,22 +49,6 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// snapshot returns a point-in-time copy of the tracked container list
|
||||
// (the slice of *Stats pointers). The returned slice is safe for use
|
||||
// without holding the stats lock, but the underlying Stats values may
|
||||
// continue to change concurrently.
|
||||
func (s *stats) snapshot() []*Stats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if len(s.cs) == 0 {
|
||||
return nil
|
||||
}
|
||||
// https://github.com/golang/go/issues/53643
|
||||
cp := make([]*Stats, len(s.cs))
|
||||
copy(cp, s.cs)
|
||||
return cp
|
||||
}
|
||||
|
||||
func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) { //nolint:gocyclo
|
||||
var getFirst bool
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package container
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package command
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package context
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package context
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package context
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package context
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package context
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func newUseCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "use CONTEXT",
|
||||
Short: "Set the default docker context",
|
||||
Short: "Set the current docker context",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package command
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package command
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package command
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -21,14 +21,13 @@ import (
|
||||
const (
|
||||
defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
|
||||
|
||||
namesHeader = "NAMES"
|
||||
commandHeader = "COMMAND"
|
||||
runningForHeader = "CREATED"
|
||||
mountsHeader = "MOUNTS"
|
||||
localVolumes = "LOCAL VOLUMES"
|
||||
networksHeader = "NETWORKS"
|
||||
platformHeader = "PLATFORM"
|
||||
healthStatusHeader = "HEALTH STATUS"
|
||||
namesHeader = "NAMES"
|
||||
commandHeader = "COMMAND"
|
||||
runningForHeader = "CREATED"
|
||||
mountsHeader = "MOUNTS"
|
||||
localVolumes = "LOCAL VOLUMES"
|
||||
networksHeader = "NETWORKS"
|
||||
platformHeader = "PLATFORM"
|
||||
)
|
||||
|
||||
// Platform wraps a [ocispec.Platform] to implement the stringer interface.
|
||||
@@ -122,7 +121,6 @@ func NewContainerContext() *ContainerContext {
|
||||
"LocalVolumes": localVolumes,
|
||||
"Networks": networksHeader,
|
||||
"Platform": platformHeader,
|
||||
"HealthStatus": healthStatusHeader,
|
||||
}
|
||||
return &containerCtx
|
||||
}
|
||||
@@ -143,36 +141,25 @@ func (c *ContainerContext) ID() string {
|
||||
|
||||
// Names returns a comma-separated string of the container's names, with their
|
||||
// slash (/) prefix stripped. Additional names for the container (related to the
|
||||
// legacy `--link` feature) are omitted when formatting "truncated".
|
||||
// legacy `--link` feature) are omitted.
|
||||
func (c *ContainerContext) Names() string {
|
||||
var b strings.Builder
|
||||
for i, n := range c.c.Names {
|
||||
name := strings.TrimPrefix(n, "/")
|
||||
if c.trunc {
|
||||
// When printing truncated, we only print a single name.
|
||||
//
|
||||
// Pick the first name that's not a legacy link (does not have
|
||||
// slashes inside the name itself (e.g., "/other-container/link")).
|
||||
// Normally this would be the first name found.
|
||||
if strings.IndexByte(name, '/') == -1 {
|
||||
return name
|
||||
names := StripNamePrefix(c.c.Names)
|
||||
if c.trunc {
|
||||
for _, name := range names {
|
||||
if len(strings.Split(name, "/")) == 1 {
|
||||
names = []string{name}
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString(name)
|
||||
}
|
||||
return b.String()
|
||||
return strings.Join(names, ",")
|
||||
}
|
||||
|
||||
// StripNamePrefix removes any "/" prefix from container names returned
|
||||
// by the "ContainersList" API.
|
||||
// StripNamePrefix removes prefix from string, typically container names as returned by `ContainersList` API
|
||||
func StripNamePrefix(ss []string) []string {
|
||||
sss := make([]string, len(ss))
|
||||
for i, s := range ss {
|
||||
sss[i] = strings.TrimPrefix(s, "/")
|
||||
sss[i] = s[1:]
|
||||
}
|
||||
return sss
|
||||
}
|
||||
@@ -354,35 +341,6 @@ func (c *ContainerContext) Networks() string {
|
||||
return strings.Join(networks, ",")
|
||||
}
|
||||
|
||||
// HealthStatus returns the container's health status (for example, "healthy","unhealthy", or "starting").
|
||||
// If no healthcheck is configured, an empty
|
||||
// string is returned.
|
||||
func (c *ContainerContext) HealthStatus() string {
|
||||
if c.c.Health != nil && c.c.Health.Status != "" {
|
||||
return string(c.c.Health.Status)
|
||||
}
|
||||
|
||||
// Fallback for API versions before v1.52, which include health only in Status text;
|
||||
// see https://github.com/moby/moby/pull/50281
|
||||
// see https://github.com/moby/moby/blob/docker-v29.4.3/daemon/container/health.go#L18-L43
|
||||
_, health, ok := strings.Cut(c.c.Status, "(")
|
||||
if !ok || !strings.HasSuffix(health, ")") {
|
||||
return ""
|
||||
}
|
||||
|
||||
health = strings.TrimSuffix(health, ")")
|
||||
health = strings.TrimPrefix(health, "health: ")
|
||||
|
||||
switch container.HealthStatus(health) {
|
||||
case container.Healthy, container.Unhealthy, container.Starting:
|
||||
return health
|
||||
case container.NoHealthcheck:
|
||||
return ""
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayablePorts returns formatted string representing open ports of container
|
||||
// e.g. "0.0.0.0:80->9090/tcp, 9988/tcp"
|
||||
// it's used by command 'docker ps'
|
||||
@@ -458,7 +416,7 @@ func comparePorts(i, j container.PortSummary) bool {
|
||||
}
|
||||
|
||||
if i.IP != j.IP {
|
||||
return i.IP.Less(j.IP)
|
||||
return i.IP.String() < j.IP.String()
|
||||
}
|
||||
|
||||
if i.PublicPort != j.PublicPort {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -494,7 +494,6 @@ func TestContainerContextWriteJSON(t *testing.T) {
|
||||
{
|
||||
"Command": `""`,
|
||||
"CreatedAt": expectedCreated,
|
||||
"HealthStatus": "",
|
||||
"ID": "containerID1",
|
||||
"Image": "ubuntu",
|
||||
"Labels": "",
|
||||
@@ -512,7 +511,6 @@ func TestContainerContextWriteJSON(t *testing.T) {
|
||||
{
|
||||
"Command": `""`,
|
||||
"CreatedAt": expectedCreated,
|
||||
"HealthStatus": "",
|
||||
"ID": "containerID2",
|
||||
"Image": "ubuntu",
|
||||
"Labels": "",
|
||||
@@ -530,7 +528,6 @@ func TestContainerContextWriteJSON(t *testing.T) {
|
||||
{
|
||||
"Command": `""`,
|
||||
"CreatedAt": expectedCreated,
|
||||
"HealthStatus": "",
|
||||
"ID": "containerID3",
|
||||
"Image": "ubuntu",
|
||||
"Labels": "",
|
||||
@@ -618,7 +615,6 @@ func TestContainerBackCompat(t *testing.T) {
|
||||
{field: "Image", expected: "docker.io/library/ubuntu"},
|
||||
{field: "Command", expected: `"/bin/sh"`},
|
||||
{field: "CreatedAt", expected: time.Unix(createdAtTime.Unix(), 0).String()},
|
||||
{field: "HealthStatus", expected: ""},
|
||||
{field: "RunningFor", expected: "12 months ago"},
|
||||
{field: "Ports", expected: "8080/tcp"},
|
||||
{field: "Status", expected: "running"},
|
||||
@@ -946,24 +942,6 @@ func TestDisplayablePorts(t *testing.T) {
|
||||
},
|
||||
expected: "80/tcp, 80/udp, 1024/tcp, 1024/udp, 12345/sctp, 1.1.1.1:1024->80/tcp, 1.1.1.1:1024->80/udp, 2.1.1.1:1024->80/tcp, 2.1.1.1:1024->80/udp, 1.1.1.1:80->1024/tcp, 1.1.1.1:80->1024/udp, 2.1.1.1:80->1024/tcp, 2.1.1.1:80->1024/udp", //nolint:revive // ignore line-length-limit (revive)
|
||||
},
|
||||
{
|
||||
// host IPs are ordered numerically, not lexicographically:
|
||||
// "10.0.0.2" sorts as a string before "9.0.0.1".
|
||||
ports: []container.PortSummary{
|
||||
{
|
||||
IP: netip.MustParseAddr("10.0.0.2"),
|
||||
PublicPort: 8080,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
}, {
|
||||
IP: netip.MustParseAddr("9.0.0.1"),
|
||||
PublicPort: 8081,
|
||||
PrivatePort: 80,
|
||||
Type: "tcp",
|
||||
},
|
||||
},
|
||||
expected: "9.0.0.1:8081->80/tcp, 10.0.0.2:8080->80/tcp",
|
||||
},
|
||||
}
|
||||
|
||||
for _, port := range cases {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ func (ctx *DiskUsageContext) startSubsection(format Format) (*template.Template,
|
||||
ctx.buffer = &bytes.Buffer{}
|
||||
ctx.header = ""
|
||||
ctx.Format = format
|
||||
ctx.preFormat()
|
||||
|
||||
return ctx.parseFormat()
|
||||
}
|
||||
@@ -87,6 +88,7 @@ func (ctx *DiskUsageContext) Write() (err error) {
|
||||
return ctx.verboseWrite()
|
||||
}
|
||||
ctx.buffer = &bytes.Buffer{}
|
||||
ctx.preFormat()
|
||||
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
@@ -211,6 +213,7 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
return ctx.verboseWriteTable(duc)
|
||||
}
|
||||
|
||||
ctx.preFormat()
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -33,7 +33,7 @@ func (f Format) IsTable() bool {
|
||||
return strings.HasPrefix(string(f), TableFormatKey)
|
||||
}
|
||||
|
||||
// IsJSON returns true if the format is the JSON format
|
||||
// IsJSON returns true if the format is the json format
|
||||
func (f Format) IsJSON() bool {
|
||||
return string(f) == JSONFormatKey
|
||||
}
|
||||
@@ -43,31 +43,6 @@ func (f Format) Contains(sub string) bool {
|
||||
return strings.Contains(string(f), sub)
|
||||
}
|
||||
|
||||
// templateString pre-processes the format and returns it as a string
|
||||
// for templating.
|
||||
func (f Format) templateString() string {
|
||||
out := string(f)
|
||||
switch out {
|
||||
case TableFormatKey:
|
||||
// A bare "--format table" should already be handled before we
|
||||
// hit this; a literal "table" here means a custom "table" format
|
||||
// without template.
|
||||
return ""
|
||||
case JSONFormatKey:
|
||||
// "--format json" only; not JSON formats ("--format '{{json .Field}}'").
|
||||
return JSONFormat
|
||||
}
|
||||
|
||||
// "--format 'table {{.Field}}\t{{.Field}}'" -> "{{.Field}}\t{{.Field}}"
|
||||
if after, isTable := strings.CutPrefix(out, TableFormatKey); isTable {
|
||||
out = after
|
||||
}
|
||||
|
||||
out = strings.Trim(out, " ") // trim spaces, but preserve other whitespace.
|
||||
out = strings.NewReplacer(`\t`, "\t", `\n`, "\n").Replace(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Context contains information required by the formatter to print the output as desired.
|
||||
type Context struct {
|
||||
// Output is the output stream to which the formatted string is written.
|
||||
@@ -78,12 +53,28 @@ type Context struct {
|
||||
Trunc bool
|
||||
|
||||
// internal element
|
||||
header any
|
||||
buffer *bytes.Buffer
|
||||
finalFormat string
|
||||
header any
|
||||
buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *Context) preFormat() {
|
||||
c.finalFormat = string(c.Format)
|
||||
// TODO: handle this in the Format type
|
||||
switch {
|
||||
case c.Format.IsTable():
|
||||
c.finalFormat = c.finalFormat[len(TableFormatKey):]
|
||||
case c.Format.IsJSON():
|
||||
c.finalFormat = JSONFormat
|
||||
}
|
||||
|
||||
c.finalFormat = strings.Trim(c.finalFormat, " ")
|
||||
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
|
||||
c.finalFormat = r.Replace(c.finalFormat)
|
||||
}
|
||||
|
||||
func (c *Context) parseFormat() (*template.Template, error) {
|
||||
tmpl, err := templates.Parse(c.Format.templateString())
|
||||
tmpl, err := templates.Parse(c.finalFormat)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("template parsing error: %w", err)
|
||||
}
|
||||
@@ -91,21 +82,20 @@ func (c *Context) parseFormat() (*template.Template, error) {
|
||||
}
|
||||
|
||||
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
|
||||
out := c.Output
|
||||
if out == nil {
|
||||
out = io.Discard
|
||||
if c.Output == nil {
|
||||
c.Output = io.Discard
|
||||
}
|
||||
if !c.Format.IsTable() {
|
||||
_, _ = c.buffer.WriteTo(out)
|
||||
return
|
||||
if c.Format.IsTable() {
|
||||
t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0)
|
||||
buffer := bytes.NewBufferString("")
|
||||
tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
|
||||
buffer.WriteTo(t)
|
||||
t.Write([]byte("\n"))
|
||||
c.buffer.WriteTo(t)
|
||||
t.Flush()
|
||||
} else {
|
||||
c.buffer.WriteTo(c.Output)
|
||||
}
|
||||
|
||||
// Write column-headers and rows to the tab-writer buffer, then flush the output.
|
||||
tw := tabwriter.NewWriter(out, 10, 1, 3, ' ', 0)
|
||||
_ = tmpl.Funcs(templates.HeaderFunctions).Execute(tw, subContext.FullHeader())
|
||||
_, _ = tw.Write([]byte{'\n'})
|
||||
_, _ = c.buffer.WriteTo(tw)
|
||||
_ = tw.Flush()
|
||||
}
|
||||
|
||||
func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error {
|
||||
@@ -125,6 +115,8 @@ type SubFormat func(func(SubContext) error) error
|
||||
// Write the template to the buffer using this Context
|
||||
func (c *Context) Write(sub SubContext, f SubFormat) error {
|
||||
c.buffer = &bytes.Buffer{}
|
||||
c.preFormat()
|
||||
|
||||
tmpl, err := c.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -8,75 +8,20 @@ import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
f Format
|
||||
isJSON bool
|
||||
isTable bool
|
||||
template string
|
||||
}{
|
||||
{
|
||||
doc: "json format",
|
||||
f: "json",
|
||||
isJSON: true,
|
||||
isTable: false,
|
||||
template: JSONFormat,
|
||||
},
|
||||
{
|
||||
doc: "empty table format (no template)",
|
||||
f: "table",
|
||||
isJSON: false,
|
||||
isTable: true,
|
||||
template: "",
|
||||
},
|
||||
{
|
||||
doc: "table with escaped tabs",
|
||||
f: "table {{.Field}}\\t{{.Field2}}",
|
||||
isJSON: false,
|
||||
isTable: true,
|
||||
template: "{{.Field}}\t{{.Field2}}",
|
||||
},
|
||||
{
|
||||
doc: "table with raw string",
|
||||
f: `table {{.Field}}\t{{.Field2}}`,
|
||||
isJSON: false,
|
||||
isTable: true,
|
||||
template: "{{.Field}}\t{{.Field2}}",
|
||||
},
|
||||
{
|
||||
doc: "other format",
|
||||
f: "other",
|
||||
isJSON: false,
|
||||
isTable: false,
|
||||
template: "other",
|
||||
},
|
||||
{
|
||||
doc: "other with spaces",
|
||||
f: " other ",
|
||||
isJSON: false,
|
||||
isTable: false,
|
||||
template: "other",
|
||||
},
|
||||
{
|
||||
doc: "other with newline preserved",
|
||||
f: " other\n ",
|
||||
isJSON: false,
|
||||
isTable: false,
|
||||
template: "other\n",
|
||||
},
|
||||
}
|
||||
f := Format("json")
|
||||
assert.Assert(t, f.IsJSON())
|
||||
assert.Assert(t, !f.IsTable())
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
assert.Check(t, is.Equal(tc.f.IsJSON(), tc.isJSON))
|
||||
assert.Check(t, is.Equal(tc.f.IsTable(), tc.isTable))
|
||||
assert.Check(t, is.Equal(tc.f.templateString(), tc.template))
|
||||
})
|
||||
}
|
||||
f = Format("table")
|
||||
assert.Assert(t, !f.IsJSON())
|
||||
assert.Assert(t, f.IsTable())
|
||||
|
||||
f = Format("other")
|
||||
assert.Assert(t, !f.IsJSON())
|
||||
assert.Assert(t, !f.IsTable())
|
||||
}
|
||||
|
||||
type fakeSubContext struct {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -24,7 +24,7 @@ func MarshalJSON(x any) ([]byte, error) {
|
||||
// marshalMap marshals x to map[string]any
|
||||
func marshalMap(x any) (map[string]any, error) {
|
||||
val := reflect.ValueOf(x)
|
||||
if val.Kind() != reflect.Pointer {
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind())
|
||||
}
|
||||
if val.IsNil() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -108,7 +104,6 @@ func (c *volumeContext) Labels() string {
|
||||
for k, v := range c.v.Labels {
|
||||
joinLabels = append(joinLabels, k+"="+v)
|
||||
}
|
||||
slices.Sort(joinLabels)
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package formatter
|
||||
|
||||
@@ -48,7 +48,9 @@ func TestVolumeContext(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
ctx = c.volumeCtx
|
||||
v := c.call()
|
||||
if v != c.expValue {
|
||||
if strings.Contains(v, ",") {
|
||||
test.CompareMultipleValues(t, v, c.expValue)
|
||||
} else if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package idresolver
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ import (
|
||||
"github.com/moby/patternmatcher"
|
||||
)
|
||||
|
||||
// DefaultDockerfileName is the Default filename with Docker commands, read by docker build
|
||||
//
|
||||
// Deprecated: this const is no longer used and will be removed in the next release.
|
||||
const DefaultDockerfileName string = "Dockerfile"
|
||||
|
||||
const (
|
||||
// defaultDockerfileName is the Default filename with Docker commands, read by docker build
|
||||
defaultDockerfileName string = "Dockerfile"
|
||||
@@ -95,6 +100,17 @@ func filepathMatches(matcher *patternmatcher.PatternMatcher, file string) (bool,
|
||||
return matcher.MatchesOrParentMatches(file)
|
||||
}
|
||||
|
||||
// DetectArchiveReader detects whether the input stream is an archive or a
|
||||
// Dockerfile and returns a buffered version of input, safe to consume in lieu
|
||||
// of input. If an archive is detected, ok is set to true, and to false
|
||||
// otherwise, in which case it is safe to assume input represents the contents
|
||||
// of a Dockerfile.
|
||||
//
|
||||
// Deprecated: this utility was only used internally, and will be removed in the next release.
|
||||
func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err error) {
|
||||
return detectArchiveReader(input)
|
||||
}
|
||||
|
||||
// detectArchiveReader detects whether the input stream is an archive or a
|
||||
// Dockerfile and returns a buffered version of input, safe to consume in lieu
|
||||
// of input. If an archive is detected, ok is set to true, and to false
|
||||
@@ -111,6 +127,15 @@ func detectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, ok bool, err er
|
||||
return newReadCloserWrapper(buf, func() error { return input.Close() }), isArchive(magic), nil
|
||||
}
|
||||
|
||||
// WriteTempDockerfile writes a Dockerfile stream to a temporary file with a
|
||||
// name specified by defaultDockerfileName and returns the path to the
|
||||
// temporary directory containing the Dockerfile.
|
||||
//
|
||||
// Deprecated: this utility was only used internally, and will be removed in the next release.
|
||||
func WriteTempDockerfile(rc io.ReadCloser) (dockerfileDir string, err error) {
|
||||
return writeTempDockerfile(rc)
|
||||
}
|
||||
|
||||
// writeTempDockerfile writes a Dockerfile stream to a temporary file with a
|
||||
// name specified by defaultDockerfileName and returns the path to the
|
||||
// temporary directory containing the Dockerfile.
|
||||
@@ -176,6 +201,14 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC
|
||||
}), defaultDockerfileName, nil
|
||||
}
|
||||
|
||||
// IsArchive checks for the magic bytes of a tar or any supported compression
|
||||
// algorithm.
|
||||
//
|
||||
// Deprecated: this utility was used internally and will be removed in the next release.
|
||||
func IsArchive(header []byte) bool {
|
||||
return isArchive(header)
|
||||
}
|
||||
|
||||
// isArchive checks for the magic bytes of a tar or any supported compression
|
||||
// algorithm.
|
||||
func isArchive(header []byte) bool {
|
||||
@@ -272,6 +305,17 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (string, string, er
|
||||
return localDir, relDockerfile, err
|
||||
}
|
||||
|
||||
// ResolveAndValidateContextPath uses the given context directory for a `docker build`
|
||||
// and returns the absolute path to the context directory.
|
||||
//
|
||||
// Deprecated: this utility was used internally and will be removed in the next
|
||||
// release. Use [DetectContextType] to detect the context-type, and use
|
||||
// [GetContextFromLocalDir], [GetContextFromLocalDir], [GetContextFromGitURL],
|
||||
// or [GetContextFromURL] instead.
|
||||
func ResolveAndValidateContextPath(givenContextDir string) (string, error) {
|
||||
return resolveAndValidateContextPath(givenContextDir)
|
||||
}
|
||||
|
||||
// resolveAndValidateContextPath uses the given context directory for a `docker build`
|
||||
// and returns the absolute path to the context directory.
|
||||
func resolveAndValidateContextPath(givenContextDir string) (string, error) {
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) {
|
||||
contextDir := createTestTempDir(t)
|
||||
createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents)
|
||||
|
||||
t.Chdir(contextDir)
|
||||
chdir(t, contextDir)
|
||||
|
||||
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
|
||||
assert.NilError(t, err)
|
||||
@@ -110,7 +110,7 @@ func TestGetContextFromLocalDirLocalFile(t *testing.T) {
|
||||
|
||||
func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) {
|
||||
contextDir := createTestTempDir(t)
|
||||
t.Chdir(contextDir)
|
||||
chdir(t, contextDir)
|
||||
|
||||
createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents)
|
||||
|
||||
@@ -248,6 +248,20 @@ func createTestTempFile(t *testing.T, dir, filename, contents string) string {
|
||||
return filePath
|
||||
}
|
||||
|
||||
// chdir changes current working directory to dir.
|
||||
// It returns a function which changes working directory back to the previous one.
|
||||
// This function is meant to be executed as a deferred call.
|
||||
// When an error occurs, it terminates the test.
|
||||
func chdir(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
workingDirectory, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, os.Chdir(dir))
|
||||
t.Cleanup(func() {
|
||||
assert.NilError(t, os.Chdir(workingDirectory))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsArchive(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package image
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package image
|
||||
|
||||
@@ -211,6 +211,6 @@ func printAmbiguousHint(stdErr io.Writer, matchName string) {
|
||||
"save",
|
||||
"tag":
|
||||
|
||||
_, _ = fmt.Fprintf(stdErr, "No images found matching %q: did you mean \"docker image %[1]s\"?\n", matchName)
|
||||
_, _ = fmt.Fprintf(stdErr, "\nNo images found matching %q: did you mean \"docker image %[1]s\"?\n", matchName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package image
|
||||
|
||||
@@ -123,7 +123,6 @@ To push the complete multi-platform image, remove the --platform flag.
|
||||
return err
|
||||
}
|
||||
|
||||
var notes []string
|
||||
defer func() {
|
||||
_ = responseBody.Close()
|
||||
for _, note := range notes {
|
||||
@@ -132,16 +131,18 @@ 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(¬es, out)))
|
||||
err = jsonstream.Display(ctx, responseBody, streams.NewOut(io.Discard), jsonstream.WithAuxCallback(handleAux()))
|
||||
if err == nil {
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), ref.String())
|
||||
}
|
||||
return err
|
||||
}
|
||||
return jsonstream.Display(ctx, responseBody, dockerCli.Out(), jsonstream.WithAuxCallback(handleAux(¬es, out)))
|
||||
return jsonstream.Display(ctx, responseBody, dockerCli.Out(), jsonstream.WithAuxCallback(handleAux()))
|
||||
}
|
||||
|
||||
func handleAux(notes *[]string, out tui.Output) func(jm jsonstream.JSONMessage) {
|
||||
var notes []string
|
||||
|
||||
func handleAux() func(jm jsonstream.JSONMessage) {
|
||||
return func(jm jsonstream.JSONMessage) {
|
||||
b := []byte(*jm.Aux)
|
||||
|
||||
@@ -149,10 +150,10 @@ func handleAux(notes *[]string, out tui.Output) 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",
|
||||
out.Color(aec.RedF).Apply(stripped.OriginalIndex.Digest.String()),
|
||||
out.Color(aec.GreenF).Apply(stripped.SelectedManifest.Digest.String()),
|
||||
aec.RedF.Apply(stripped.OriginalIndex.Digest.String()),
|
||||
aec.GreenF.Apply(stripped.SelectedManifest.Digest.String()),
|
||||
)
|
||||
*notes = append(*notes, note)
|
||||
notes = append(notes, note)
|
||||
}
|
||||
|
||||
var missing auxprogress.ContentMissing
|
||||
@@ -165,7 +166,7 @@ func handleAux(notes *[]string, out tui.Output) 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,18 +1,13 @@
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -91,28 +86,3 @@ 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)
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
WARNING: This output is designed for human readability. For machine-readable output, please use --format.
|
||||
|
||||
No images found matching "ls": did you mean "docker image ls"?
|
||||
|
||||
+37
-25
@@ -1,17 +1,19 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package image
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/go-units"
|
||||
imagetypes "github.com/moby/moby/api/types/image"
|
||||
@@ -204,41 +206,37 @@ var allChips = []imageChip{
|
||||
chipInUse,
|
||||
}
|
||||
|
||||
// getPossibleChips returns the list of chips used by at least one image.
|
||||
// It is used to determine which columns to print (and how much width to
|
||||
// reserve).
|
||||
func getPossibleChips(view treeView) (chips []imageChip) {
|
||||
remaining := slices.Clone(allChips)
|
||||
|
||||
check := func(d imageDetails) (done bool) {
|
||||
// filter without allocating
|
||||
out := remaining[:0]
|
||||
for _, chip := range remaining {
|
||||
if chip.check(&d) {
|
||||
chips = append(chips, chip)
|
||||
continue
|
||||
}
|
||||
out = append(out, chip)
|
||||
}
|
||||
remaining = out
|
||||
return len(remaining) == 0
|
||||
}
|
||||
remaining := make([]imageChip, len(allChips))
|
||||
copy(remaining, allChips)
|
||||
|
||||
var possible []imageChip
|
||||
for _, img := range view.images {
|
||||
if check(img.Details) {
|
||||
return chips
|
||||
}
|
||||
details := []imageDetails{img.Details}
|
||||
|
||||
for _, c := range img.Children {
|
||||
if check(c.Details) {
|
||||
return chips
|
||||
details = append(details, c.Details)
|
||||
}
|
||||
|
||||
for _, d := range details {
|
||||
for idx := len(remaining) - 1; idx >= 0; idx-- {
|
||||
chip := remaining[idx]
|
||||
if chip.check(&d) {
|
||||
possible = append(possible, chip)
|
||||
remaining = append(remaining[:idx], remaining[idx+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chips
|
||||
return possible
|
||||
}
|
||||
|
||||
func printImageTree(outs command.Streams, view treeView) {
|
||||
if streamRedirected(outs.Out()) {
|
||||
_, _ = fmt.Fprintln(outs.Err(), "WARNING: This output is designed for human readability. For machine-readable output, please use --format.")
|
||||
}
|
||||
|
||||
out := tui.NewOutput(outs.Out())
|
||||
isTerm := out.IsTerminal()
|
||||
|
||||
@@ -563,3 +561,17 @@ func widestFirstColumnValue(headers []imgColumn, images []topImage) int {
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func streamRedirected(s *streams.Out) bool {
|
||||
fd := s.FD()
|
||||
if os.Stdout.Fd() != fd {
|
||||
return true
|
||||
}
|
||||
|
||||
fi, err := os.Stdout.Stat()
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return fi.Mode()&os.ModeCharDevice == 0
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package inspect
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package manifest
|
||||
|
||||
@@ -48,6 +48,27 @@ func newManifestStore(dockerCLI command.Cli) store.Store {
|
||||
return store.NewStore(filepath.Join(config.Dir(), "manifests"))
|
||||
}
|
||||
|
||||
// authConfigKey is the key used to store credentials for Docker Hub. It is
|
||||
// a copy of [registry.IndexServer].
|
||||
//
|
||||
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
|
||||
const authConfigKey = "https://index.docker.io/v1/"
|
||||
|
||||
// getAuthConfigKey special-cases using the full index address of the official
|
||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||
//
|
||||
// It is similar to [registry.GetAuthConfigKey], but does not require on
|
||||
// [registrytypes.IndexInfo] as intermediate.
|
||||
//
|
||||
// [registry.GetAuthConfigKey]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#GetAuthConfigKey
|
||||
// [registrytypes.IndexInfo]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/api/types/registry#IndexInfo
|
||||
func getAuthConfigKey(domainName string) string {
|
||||
if domainName == "docker.io" || domainName == "index.docker.io" {
|
||||
return authConfigKey
|
||||
}
|
||||
return domainName
|
||||
}
|
||||
|
||||
// newRegistryClient returns a client for communicating with a Docker distribution
|
||||
// registry
|
||||
func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient.RegistryClient {
|
||||
@@ -57,7 +78,8 @@ func newRegistryClient(dockerCLI command.Cli, allowInsecure bool) registryclient
|
||||
}
|
||||
cfg := dockerCLI.ConfigFile()
|
||||
resolver := func(ctx context.Context, domainName string) registry.AuthConfig {
|
||||
a, _ := cfg.GetAuthConfig(domainName)
|
||||
configKey := getAuthConfigKey(domainName)
|
||||
a, _ := cfg.GetAuthConfig(configKey)
|
||||
return registry.AuthConfig{
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
|
||||
package network
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -119,7 +115,6 @@ func (c *networkContext) Labels() string {
|
||||
for k, v := range c.n.Labels {
|
||||
joinLabels = append(joinLabels, k+"="+v)
|
||||
}
|
||||
slices.Sort(joinLabels)
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package network
|
||||
|
||||
@@ -68,7 +68,9 @@ func TestNetworkContext(t *testing.T) {
|
||||
for _, c := range cases {
|
||||
ctx = c.networkCtx
|
||||
v := c.call()
|
||||
if v != c.expValue {
|
||||
if strings.Contains(v, ",") {
|
||||
test.CompareMultipleValues(t, v, c.expValue)
|
||||
} else if v != c.expValue {
|
||||
t.Fatalf("Expected %s, was %s\n", c.expValue, v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package network
|
||||
|
||||
|
||||
@@ -2,26 +2,12 @@ package node
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// nodePsFilters are the filters that can be used with "docker node ps --filter".
|
||||
nodePsFilters = []string{"desired-state", "id", "label", "name"}
|
||||
|
||||
// taskDesiredStates are the valid values for the "desired-state" task filter.
|
||||
taskDesiredStates = []string{
|
||||
string(swarm.TaskStateRunning),
|
||||
string(swarm.TaskStateShutdown),
|
||||
string(swarm.TaskStateAccepted),
|
||||
}
|
||||
)
|
||||
|
||||
// completeNodeNames offers completion for swarm node (host)names and optional IDs.
|
||||
// By default, only names are returned.
|
||||
// Set DOCKER_COMPLETION_SHOW_NODE_IDS=yes to also complete IDs.
|
||||
@@ -49,23 +35,3 @@ func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionF
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// completeNodePsFilters provides completion for the filters that can be used
|
||||
// with "docker node ps --filter".
|
||||
func completeNodePsFilters(_ completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
switch key {
|
||||
case "desired-state":
|
||||
return completion.WithPrefix("desired-state=", taskDesiredStates), cobra.ShellCompDirectiveNoFileComp
|
||||
case "id", "name", "label":
|
||||
// Task IDs, names, and labels are not easily discoverable; only offer the key.
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
default:
|
||||
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCompleteNodePsFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
toComplete string
|
||||
expected []string
|
||||
directive cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no input offers the filter keys",
|
||||
toComplete: "",
|
||||
expected: []string{"desired-state=", "id=", "label=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
doc: "desired-state values",
|
||||
toComplete: "desired-state=",
|
||||
expected: []string{"desired-state=running", "desired-state=shutdown", "desired-state=accepted"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "label offers no values",
|
||||
toComplete: "label=",
|
||||
expected: nil,
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "unknown key falls back to the filter keys",
|
||||
toComplete: "bogus=",
|
||||
expected: []string{"desired-state=", "id=", "label=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
completions, directive := completeNodePsFilters(cli)(newPsCommand(cli), nil, tc.toComplete)
|
||||
assert.DeepEqual(t, completions, tc.expected)
|
||||
assert.Equal(t, directive, tc.directive)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package node
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package node
|
||||
|
||||
|
||||
@@ -48,8 +48,6 @@ func newPsCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template")
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("filter", completeNodePsFilters(dockerCLI))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package node
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package plugin
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.25
|
||||
//go:build go1.24
|
||||
|
||||
package plugin
|
||||
|
||||
|
||||
+24
-13
@@ -97,22 +97,17 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
|
||||
// If defaultUsername is not empty, the username prompt includes that username
|
||||
// and the user can hit enter without inputting a username to use that default
|
||||
// username.
|
||||
func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
|
||||
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
|
||||
// On Windows, force the use of the regular OS stdin stream.
|
||||
//
|
||||
// StdStreams() may wrap stdin with windowsconsole.NewAnsiReader to
|
||||
// emulate VT input on consoles that do not support it natively, but
|
||||
// that wrapper has historically caused interactive prompts to hang
|
||||
// or behave incorrectly.
|
||||
//
|
||||
// See:
|
||||
// - https://github.com/moby/moby/issues/14336
|
||||
// - https://github.com/moby/moby/issues/14210
|
||||
// - https://github.com/moby/moby/pull/17738
|
||||
stdIn := cli.In()
|
||||
//
|
||||
// TODO(thaJeztah): we need to confirm if this special handling is still needed, as we may not be doing this in other places.
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO(thaJeztah); change to io.Reader and skip wrapping once prompt.DisableInputEcho no longer requires a streams.In
|
||||
stdIn = streams.NewIn(os.Stdin)
|
||||
cli.SetIn(streams.NewIn(os.Stdin))
|
||||
}
|
||||
|
||||
argUser = strings.TrimSpace(argUser)
|
||||
@@ -137,7 +132,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
|
||||
}
|
||||
|
||||
var err error
|
||||
argUser, err = prompt.ReadInput(ctx, stdIn, cli.Out(), msg)
|
||||
argUser, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), msg)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@@ -151,7 +146,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
|
||||
|
||||
isEmpty := strings.TrimSpace(argPassword) == ""
|
||||
if isEmpty {
|
||||
restoreInput, err := prompt.DisableInputEcho(stdIn)
|
||||
restoreInput, err := prompt.DisableInputEcho(cli.In())
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@@ -171,7 +166,7 @@ func PromptUserForCredentials(ctx context.Context, cli Streams, argUser, argPass
|
||||
"To create a PAT, visit " + aec.Underline.Apply("https://app.docker.com/settings") + "\n\n")
|
||||
}
|
||||
|
||||
argPassword, err = prompt.ReadInput(ctx, stdIn, cli.Out(), "Password: ")
|
||||
argPassword, err = prompt.ReadInput(ctx, cli.In(), cli.Out(), "Password: ")
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{}, err
|
||||
}
|
||||
@@ -199,7 +194,8 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
authConfig, err := cfg.GetAuthConfig(reference.Domain(registryRef))
|
||||
configKey := getAuthConfigKey(reference.Domain(registryRef))
|
||||
authConfig, err := cfg.GetAuthConfig(configKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -215,3 +211,18 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
RegistryToken: authConfig.RegistryToken,
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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), or "-" to read from stdin`)
|
||||
flags.StringVarP(&opts.password, "password", "p", "", "Password or Personal Access Token (PAT)")
|
||||
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") || opts.password == "-" {
|
||||
if flags.Changed("password") && opts.password != "-" {
|
||||
if flags.Changed("password-stdin") {
|
||||
if flags.Changed("password") {
|
||||
return errors.New("conflicting options: cannot specify both --password and --password-stdin")
|
||||
}
|
||||
if !flags.Changed("username") {
|
||||
@@ -122,11 +122,6 @@ 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,57 +339,6 @@ 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{},
|
||||
@@ -448,7 +397,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 || tc.input.password == "-" || tc.stdIn != "" {
|
||||
if tc.input.passwordStdin {
|
||||
if tc.expectedErr == "TEST_READ_ERR" {
|
||||
cli.SetIn(streams.NewIn(io.NopCloser(iotest.ErrReader(errors.New(tc.expectedErr)))))
|
||||
} else {
|
||||
@@ -683,21 +632,6 @@ 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", ""},
|
||||
@@ -724,10 +658,3 @@ 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, `"-"`))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user