mirror of
https://github.com/docker/cli.git
synced 2026-08-24 10:05:37 -05:00
Compare commits
@@ -1,17 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: "🗃️ Docker Sandboxes: report an issue"
|
||||
about: "Issues with Docker Sandboxes should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
||||
url: "https://github.com/docker/desktop-feedback/issues"
|
||||
- name: "🖥️ Docker Desktop: report an issue"
|
||||
about: "General Docker Desktop issues (installation, upgrades, UI, networking on macOS/Windows, WSL2, etc.) should be filed in the Docker Desktop feedback tracker (not docker/cli)."
|
||||
url: "https://github.com/docker/desktop-feedback/issues"
|
||||
- name: "🧑💻 Contributing to Docker"
|
||||
- name: "Contributing to Docker"
|
||||
about: "Read guidelines and tips about contributing to Docker."
|
||||
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
|
||||
- name: "🔒 Security and Vulnerabilities"
|
||||
- name: "Security and Vulnerabilities"
|
||||
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker."
|
||||
url: "https://github.com/moby/moby/security/policy"
|
||||
- name: "💬 General Support"
|
||||
- name: "General Support"
|
||||
about: "Get the help you need to build, share, and run your Docker applications"
|
||||
url: "https://www.docker.com/support/"
|
||||
|
||||
@@ -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)**
|
||||
|
||||
|
||||
+1
-10
@@ -5,14 +5,5 @@ 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@v5
|
||||
-
|
||||
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@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@v6
|
||||
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@v4
|
||||
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@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
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@v6
|
||||
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@v5
|
||||
-
|
||||
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@v3
|
||||
-
|
||||
name: Build
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
targets: plugins-cross
|
||||
set: |
|
||||
|
||||
@@ -46,10 +46,9 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v5
|
||||
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,19 @@ jobs:
|
||||
ln -s vendor.sum go.sum
|
||||
-
|
||||
name: Update Go
|
||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
go-version: "1.24.7"
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
-
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
-
|
||||
name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -37,16 +37,14 @@ jobs:
|
||||
- alpine
|
||||
- debian
|
||||
engine-version:
|
||||
- rc # latest rc
|
||||
- 29 # latest
|
||||
- 28 # latest - 1
|
||||
- 25 # mirantis lts
|
||||
- 28 # latest
|
||||
- 27 # latest - 1
|
||||
- 26 # github actions default
|
||||
- 23 # mirantis lts
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v5
|
||||
-
|
||||
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@v3
|
||||
-
|
||||
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@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"
|
||||
+10
-16
@@ -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@v3
|
||||
-
|
||||
name: Test
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@v6
|
||||
with:
|
||||
targets: test-coverage
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./build/coverage/coverage.txt
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -53,36 +53,30 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
- macos-15-intel # macOS 15 on Intel
|
||||
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
|
||||
- macos-13 # macOS 13 on Intel
|
||||
- macos-14 # macOS 14 on arm64 (Apple Silicon M1)
|
||||
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@v5
|
||||
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@v5
|
||||
with:
|
||||
go-version: "1.26.7"
|
||||
cache: false
|
||||
go-version: "1.24.7"
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
# run in go modules mode to prevent traversing to nested modules
|
||||
ln -s vendor.mod go.mod
|
||||
ln -s vendor.sum go.sum
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '^github.com/docker/cli/e2e/')
|
||||
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
go tool cover -func=/tmp/coverage.txt
|
||||
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
|
||||
shell: bash
|
||||
-
|
||||
name: Send to Codecov
|
||||
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||
uses: codecov/codecov-action@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} ✓`);
|
||||
@@ -11,23 +11,18 @@ permissions:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, labeled, unlabeled, synchronize]
|
||||
types: [opened, edited, labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check-labels:
|
||||
check-area-label:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120 # guardrails timeout for the whole job
|
||||
steps:
|
||||
- name: Missing `area/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
|
||||
exit 1
|
||||
- name: Missing `kind/` label
|
||||
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
|
||||
run: |
|
||||
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
|
||||
exit 1
|
||||
- name: OK
|
||||
run: exit 0
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Run
|
||||
uses: docker/bake-action@d3418bd7d0e9324001bca92fa8ba175ea7e6dc9b # v7.3.0
|
||||
uses: docker/bake-action@v6
|
||||
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@v5
|
||||
-
|
||||
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@v5
|
||||
-
|
||||
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
-14
@@ -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.24.7"
|
||||
|
||||
timeout: 5m
|
||||
|
||||
@@ -94,8 +94,6 @@ linters:
|
||||
desc: Use github.com/google/uuid instead.
|
||||
- pkg: "io/ioutil"
|
||||
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
|
||||
- pkg: "gopkg.in/yaml.v3"
|
||||
desc: Use go.yaml.in/yaml/v3 instead.
|
||||
|
||||
forbidigo:
|
||||
forbid:
|
||||
@@ -110,15 +108,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:
|
||||
@@ -163,7 +154,11 @@ linters:
|
||||
arguments: [200]
|
||||
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
|
||||
- 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
|
||||
@@ -227,10 +222,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>
|
||||
@@ -65,14 +64,11 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
|
||||
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
|
||||
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
|
||||
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <55906459+austinvazquez@users.noreply.github.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com> <macedonv@amazon.com>
|
||||
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
|
||||
Ben Bonnefoy <frenchben@docker.com>
|
||||
Ben Golub <ben.golub@dotcloud.com>
|
||||
@@ -154,8 +150,6 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
|
||||
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
|
||||
David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
|
||||
David M. Karr <davidmichaelkarr@gmail.com>
|
||||
@@ -356,9 +350,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 +397,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 +566,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>
|
||||
@@ -66,8 +63,6 @@ Andreas Köhler <andi5.py@gmx.net>
|
||||
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>
|
||||
@@ -91,12 +86,11 @@ Archimedes Trajano <developer@trajano.net>
|
||||
Arko Dasgupta <arko@tetrate.io>
|
||||
Arnaud Porterie <icecrime@gmail.com>
|
||||
Arnaud Rebillout <elboulangero@gmail.com>
|
||||
Arthur Flageul <arthur.flageul@gmail.com>
|
||||
Arthur Peka <arthur.peka@outlook.com>
|
||||
Ashly Mathew <ashly.mathew@sap.com>
|
||||
Ashwini Oruganti <ashwini.oruganti@gmail.com>
|
||||
Aslam Ahemad <aslamahemad@gmail.com>
|
||||
Austin Vazquez <austin.vazquez@docker.com>
|
||||
Austin Vazquez <austin.vazquez.dev@gmail.com>
|
||||
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
|
||||
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
|
||||
Barnaby Gray <barnaby@pickle.me.uk>
|
||||
@@ -131,7 +125,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>
|
||||
@@ -142,12 +135,10 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
|
||||
Carlo Mion <mion00@gmail.com>
|
||||
Carlos Alexandro Becker <caarlos0@gmail.com>
|
||||
Carlos de Paula <me@carlosedp.com>
|
||||
carsontham <carsontham@outlook.com>
|
||||
Carston Schilds <Carston.Schilds@visier.com>
|
||||
Casey Korver <casey@korver.dev>
|
||||
Ce Gao <ce.gao@outlook.com>
|
||||
Cedric Davies <cedricda@microsoft.com>
|
||||
Cesar Talledo <cesar.talledo@docker.com>
|
||||
Cezar Sa Espinola <cezarsa@gmail.com>
|
||||
Chad Faragher <wyckster@hotmail.com>
|
||||
Chao Wang <wangchao.fnst@cn.fujitsu.com>
|
||||
@@ -162,7 +153,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 +174,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>
|
||||
@@ -231,7 +220,7 @@ David Alvarez <david.alvarez@flyeralarm.com>
|
||||
David Beitey <david@davidjb.com>
|
||||
David Calavera <david.calavera@gmail.com>
|
||||
David Cramer <davcrame@cisco.com>
|
||||
David Dooling <david.dooling@docker.com>
|
||||
David Dooling <dooling@gmail.com>
|
||||
David Gageot <david@gageot.net>
|
||||
David Karlsson <david.karlsson@docker.com>
|
||||
David le Blanc <systemmonkey42@users.noreply.github.com>
|
||||
@@ -241,7 +230,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 +237,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 +256,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>
|
||||
@@ -279,7 +265,6 @@ Eli Uriegas <eli.uriegas@docker.com>
|
||||
Eli Uriegas <seemethere101@gmail.com>
|
||||
Elias Faxö <elias.faxo@tre.se>
|
||||
Elliot Luo <956941328@qq.com>
|
||||
Eng Zer Jun <engzerjun@gmail.com>
|
||||
Eric Bode <eric.bode@foundries.io>
|
||||
Eric Curtin <ericcurtin17@gmail.com>
|
||||
Eric Engestrom <eric@engestrom.ch>
|
||||
@@ -360,7 +345,6 @@ Henning Sprang <henning.sprang@gmail.com>
|
||||
Henry N <henrynmail-github@yahoo.de>
|
||||
Hernan Garcia <hernandanielg@gmail.com>
|
||||
Hongbin Lu <hongbin034@gmail.com>
|
||||
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
|
||||
Hu Keping <hukeping@huawei.com>
|
||||
Huayi Zhang <irachex@gmail.com>
|
||||
Hugo Chastel <Hugo-C@users.noreply.github.com>
|
||||
@@ -481,7 +465,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 +533,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 +545,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 +572,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>
|
||||
@@ -618,12 +595,9 @@ Michael Prokop <github@michael-prokop.at>
|
||||
Michael Scharf <github@scharf.gr>
|
||||
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 +610,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 +618,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 +669,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 +695,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 +719,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 +769,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 +869,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 +880,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>
|
||||
@@ -931,7 +896,6 @@ Wenlong Zhang <zhangwenlong@loongson.cn>
|
||||
Wenzhi Liang <wenzhi.liang@gmail.com>
|
||||
Wes Morgan <cap10morgan@gmail.com>
|
||||
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
|
||||
Will Wang <willww64@gmail.com>
|
||||
William Henry <whenry@redhat.com>
|
||||
Xianglin Gao <xlgao@zju.edu.cn>
|
||||
Xiaodong Liu <liuxiaodong@loongson.cn>
|
||||
@@ -944,7 +908,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`.
|
||||
|
||||
+12
-14
@@ -5,32 +5,26 @@ ARG BASE_VARIANT=alpine
|
||||
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
|
||||
# It must be a supported tag in the docker.io/library/alpine image repository
|
||||
# that's also available as alpine image variant for the Golang version used.
|
||||
ARG ALPINE_VERSION=3.23
|
||||
ARG ALPINE_VERSION=3.21
|
||||
ARG BASE_DEBIAN_DISTRO=bookworm
|
||||
|
||||
ARG GO_VERSION=1.26.7
|
||||
|
||||
# XX_VERSION specifies the version of the xx utility to use.
|
||||
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
|
||||
ARG XX_VERSION=1.9.0
|
||||
|
||||
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
|
||||
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
|
||||
ARG GOVERSIONINFO_VERSION=v1.5.0
|
||||
ARG GO_VERSION=1.24.7
|
||||
ARG XX_VERSION=1.6.1
|
||||
ARG GOVERSIONINFO_VERSION=v1.4.1
|
||||
|
||||
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
|
||||
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
|
||||
ARG GOTESTSUM_VERSION=v1.13.0
|
||||
ARG GOTESTSUM_VERSION=v1.12.3
|
||||
|
||||
# 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.25.0
|
||||
|
||||
# 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=v2.38.2
|
||||
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
|
||||
|
||||
@@ -97,7 +91,7 @@ ENV GO111MODULE=auto
|
||||
RUN --mount=type=bind,target=.,rw \
|
||||
--mount=type=cache,target=/root/.cache \
|
||||
--mount=type=cache,target=/go/pkg/mod \
|
||||
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')
|
||||
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
|
||||
FROM scratch AS test-coverage
|
||||
COPY --from=test /tmp/coverage.txt /coverage.txt
|
||||
@@ -122,6 +116,10 @@ FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
|
||||
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
|
||||
|
||||
FROM e2e-base-${BASE_VARIANT} AS e2e
|
||||
ARG NOTARY_VERSION=v0.6.1
|
||||
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
|
||||
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
|
||||
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
|
||||
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
|
||||
COPY --link --from=build /out ./build/
|
||||
COPY --link --from=build-plugins /out ./build/
|
||||
|
||||
@@ -34,12 +34,12 @@ test: test-unit ## run tests
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
|
||||
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS)
|
||||
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS)
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
mkdir -p $(CURDIR)/build/coverage
|
||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## run all the lint tools
|
||||
@@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
|
||||
.PHONY: fmt
|
||||
fmt: ## run gofumpt (if present) or gofmt
|
||||
@if command -v gofumpt > /dev/null; then \
|
||||
gofumpt -w -d -lang=1.24 . ; \
|
||||
gofumpt -w -d -lang=1.23 . ; \
|
||||
else \
|
||||
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
|
||||
fi
|
||||
@@ -69,15 +69,6 @@ dynbinary: ## build dynamically linked binary
|
||||
plugins: ## build example CLI plugins
|
||||
scripts/build/plugins
|
||||
|
||||
.PHONY: trust-plugin
|
||||
trust-plugin: ## build docker-trust CLI plugins
|
||||
scripts/build/trust-plugin
|
||||
|
||||
.PHONY: install-trust-plugin
|
||||
install-trust-plugin: trust-plugin
|
||||
install-trust-plugin: ## install docker-trust CLI plugins
|
||||
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust
|
||||
|
||||
.PHONY: vendor
|
||||
vendor: ## update vendor with go modules
|
||||
rm -rf vendor
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -26,7 +25,7 @@ func main() {
|
||||
Short: "Print the API version of the server",
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
ping, err := apiClient.Ping(context.Background(), client.PingOptions{})
|
||||
ping, err := apiClient.Ping(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package manager
|
||||
|
||||
import "github.com/docker/cli/cli-plugins/metadata"
|
||||
|
||||
const (
|
||||
// CommandAnnotationPlugin is added to every stub command added by
|
||||
// AddPluginCommandStubs with the value "true" and so can be
|
||||
// used to distinguish plugin stubs from regular commands.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPlugin]. This alias will be removed in the next release.
|
||||
CommandAnnotationPlugin = metadata.CommandAnnotationPlugin
|
||||
|
||||
// CommandAnnotationPluginVendor is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the vendor of
|
||||
// that plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginVendor]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginVendor = metadata.CommandAnnotationPluginVendor
|
||||
|
||||
// CommandAnnotationPluginVersion is added to every stub command
|
||||
// added by AddPluginCommandStubs and contains the version of
|
||||
// that plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginVersion]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginVersion = metadata.CommandAnnotationPluginVersion
|
||||
|
||||
// CommandAnnotationPluginInvalid is added to any stub command
|
||||
// added by AddPluginCommandStubs for an invalid command (that
|
||||
// is, one which failed it's candidate test) and contains the
|
||||
// reason for the failure.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginInvalid]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginInvalid = metadata.CommandAnnotationPluginInvalid
|
||||
|
||||
// CommandAnnotationPluginCommandPath is added to overwrite the
|
||||
// command path for a plugin invocation.
|
||||
//
|
||||
// Deprecated: use [metadata.CommandAnnotationPluginCommandPath]. This alias will be removed in the next release.
|
||||
CommandAnnotationPluginCommandPath = metadata.CommandAnnotationPluginCommandPath
|
||||
)
|
||||
@@ -32,12 +32,14 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
|
||||
func TestValidateCandidate(t *testing.T) {
|
||||
const (
|
||||
goodPluginName = metadata.NamePrefix + "goodplugin"
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
builtinName = metadata.NamePrefix + "builtin"
|
||||
builtinAlias = metadata.NamePrefix + "alias"
|
||||
|
||||
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
|
||||
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
|
||||
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
|
||||
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
|
||||
)
|
||||
|
||||
fakeroot := &cobra.Command{Use: "docker"}
|
||||
@@ -49,114 +51,42 @@ func TestValidateCandidate(t *testing.T) {
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
plugin *fakeCandidate
|
||||
name string
|
||||
c *fakeCandidate
|
||||
|
||||
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
|
||||
err string
|
||||
invalid string
|
||||
expVer string
|
||||
}{
|
||||
// Invalid cases.
|
||||
{
|
||||
name: "empty path",
|
||||
plugin: &fakeCandidate{path: ""},
|
||||
err: "plugin candidate path cannot be empty",
|
||||
},
|
||||
{
|
||||
name: "bad prefix",
|
||||
plugin: &fakeCandidate{path: badPrefixPath},
|
||||
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix),
|
||||
},
|
||||
{
|
||||
name: "bad path",
|
||||
plugin: &fakeCandidate{path: badNamePath},
|
||||
invalid: "did not match",
|
||||
},
|
||||
{
|
||||
name: "builtin command",
|
||||
plugin: &fakeCandidate{path: builtinName},
|
||||
invalid: `plugin "builtin" duplicates builtin command`,
|
||||
},
|
||||
{
|
||||
name: "builtin alias",
|
||||
plugin: &fakeCandidate{path: builtinAlias},
|
||||
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
|
||||
},
|
||||
{
|
||||
name: "fetch failure",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
|
||||
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
|
||||
},
|
||||
{
|
||||
name: "metadata not json",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
|
||||
invalid: "invalid character",
|
||||
},
|
||||
{
|
||||
name: "empty schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
|
||||
invalid: `plugin SchemaVersion version cannot be empty`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
|
||||
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
|
||||
},
|
||||
{
|
||||
name: "invalid schemaversion major",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
|
||||
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
|
||||
},
|
||||
{
|
||||
name: "no vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
{
|
||||
name: "empty vendor",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
|
||||
invalid: "plugin metadata does not define a vendor",
|
||||
},
|
||||
|
||||
// Valid cases.
|
||||
{
|
||||
name: "valid",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// Including the deprecated "experimental" field should not break processing.
|
||||
name: "with legacy experimental",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
|
||||
expVer: "0.1.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new minor schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "0.2.0",
|
||||
},
|
||||
{
|
||||
// note that this may not be supported by older CLIs
|
||||
name: "new major schema version",
|
||||
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
|
||||
expVer: "1.0.0",
|
||||
},
|
||||
/* Each failing one of the tests */
|
||||
{name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
|
||||
{name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix)},
|
||||
{name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
|
||||
{name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
|
||||
{name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
|
||||
{name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
|
||||
{name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
|
||||
{name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
|
||||
{name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
|
||||
{name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
{name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
|
||||
// This one should work
|
||||
{name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
|
||||
{name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p, err := newPlugin(tc.plugin, fakeroot.Commands())
|
||||
p, err := newPlugin(tc.c, fakeroot.Commands())
|
||||
switch {
|
||||
case tc.err != "":
|
||||
assert.ErrorContains(t, err, tc.err)
|
||||
case tc.invalid != "":
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeFor[*pluginError]()))
|
||||
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
|
||||
assert.ErrorContains(t, p.Err, tc.invalid)
|
||||
default:
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName)
|
||||
assert.Equal(t, p.SchemaVersion, tc.expVer)
|
||||
assert.Equal(t, p.SchemaVersion, "0.1.0")
|
||||
assert.Equal(t, p.Vendor, "e2e-testing")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -38,14 +38,14 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||
rootCmd.AddCommand(&cobra.Command{
|
||||
Use: p.Name,
|
||||
Short: p.ShortDescription,
|
||||
Hidden: p.Hidden,
|
||||
Run: func(_ *cobra.Command, _ []string) {},
|
||||
Annotations: annotations,
|
||||
DisableFlagParsing: true,
|
||||
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") {
|
||||
@@ -56,14 +56,10 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// Delegate completion to plugin
|
||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things.
|
||||
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
|
||||
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.23
|
||||
|
||||
package manager
|
||||
|
||||
@@ -28,7 +28,7 @@ func (e *pluginError) Unwrap() error {
|
||||
return e.cause
|
||||
}
|
||||
|
||||
// MarshalText marshals the pluginError into a textual form.
|
||||
// MarshalText marshalls the pluginError into a textual form.
|
||||
func (e *pluginError) MarshalText() (text []byte, err error) {
|
||||
return []byte(e.cause.Error()), nil
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
// 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 manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
@@ -22,11 +16,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
|
||||
@@ -41,11 +39,11 @@ func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd,
|
||||
|
||||
// RunPluginHooks is the entrypoint for the hooks execution flow
|
||||
// after a plugin command was just executed by the CLI.
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
|
||||
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string) {
|
||||
commandName := strings.Join(args, " ")
|
||||
flags := getNaiveFlags(args)
|
||||
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
|
||||
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, "")
|
||||
}
|
||||
|
||||
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
|
||||
@@ -54,8 +52,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 +66,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)
|
||||
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
|
||||
@@ -152,34 +135,14 @@ func appendNextSteps(nextSteps []string, processed []string) ([]string, bool) {
|
||||
// command being executed (such as 'image ls' – the root 'docker' is omitted)
|
||||
// and, if the configuration includes a hook for the invoked command, returns
|
||||
// the configured hook string.
|
||||
//
|
||||
// Plugins can declare two types of hooks in their configuration:
|
||||
// - "hooks": fires on every command invocation (success or failure)
|
||||
// - "error-hooks": fires only when a command fails (cmdErrorMessage is non-empty)
|
||||
func pluginMatch(pluginCfg map[string]string, subCmd string, cmdErrorMessage string) (string, bool) {
|
||||
// Check "hooks" first — these always fire regardless of command outcome.
|
||||
if match, ok := matchHookConfig(pluginCfg["hooks"], subCmd); ok {
|
||||
return match, true
|
||||
}
|
||||
|
||||
// Check "error-hooks" — these only fire when there was an error.
|
||||
if cmdErrorMessage != "" {
|
||||
if match, ok := matchHookConfig(pluginCfg["error-hooks"], subCmd); ok {
|
||||
return match, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// matchHookConfig checks if a comma-separated hook configuration string
|
||||
// contains a prefix match for the given subcommand.
|
||||
func matchHookConfig(configuredHooks string, subCmd string) (string, bool) {
|
||||
if configuredHooks == "" {
|
||||
func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) {
|
||||
configuredPluginHooks, ok := pluginCfg["hooks"]
|
||||
if !ok || configuredPluginHooks == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
|
||||
commands := strings.Split(configuredPluginHooks, ",")
|
||||
for _, hookCmd := range commands {
|
||||
if hookMatch(hookCmd, subCmd) {
|
||||
return hookCmd, true
|
||||
}
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
type fakeConfigProvider struct {
|
||||
cfg *configfile.ConfigFile
|
||||
}
|
||||
|
||||
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
|
||||
return f.cfg
|
||||
}
|
||||
|
||||
func TestGetNaiveFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
@@ -51,15 +40,12 @@ func TestGetNaiveFlags(t *testing.T) {
|
||||
|
||||
func TestPluginMatch(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
commandString string
|
||||
pluginConfig map[string]string
|
||||
cmdErrorMessage string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
commandString string
|
||||
pluginConfig map[string]string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
}{
|
||||
{
|
||||
doc: "hooks prefix match",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
@@ -68,7 +54,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks no match",
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
@@ -77,7 +62,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks exact match",
|
||||
commandString: "context ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "context ls",
|
||||
@@ -86,7 +70,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks first match wins",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image ls,image",
|
||||
@@ -95,7 +78,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks empty string",
|
||||
commandString: "image ls",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "",
|
||||
@@ -104,7 +86,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks partial token no match",
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image i",
|
||||
@@ -113,7 +94,6 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks prefix token match",
|
||||
commandString: "image inspect",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "image",
|
||||
@@ -121,140 +101,12 @@ func TestPluginMatch(t *testing.T) {
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks match on error",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks no match on success",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks prefix match on error",
|
||||
commandString: "compose up",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "compose",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "compose",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks no match for wrong command",
|
||||
commandString: "pull",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "hooks takes precedence over error-hooks",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "hooks fires on success even with error-hooks configured",
|
||||
commandString: "build",
|
||||
pluginConfig: map[string]string{
|
||||
"hooks": "build",
|
||||
"error-hooks": "build",
|
||||
},
|
||||
cmdErrorMessage: "",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "error-hooks with multiple commands",
|
||||
commandString: "compose up",
|
||||
pluginConfig: map[string]string{
|
||||
"error-hooks": "build,compose up,pull",
|
||||
},
|
||||
cmdErrorMessage: "exit status 1",
|
||||
expectedMatch: "compose up",
|
||||
expectedOk: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchHookConfig(t *testing.T) {
|
||||
testCases := []struct {
|
||||
doc string
|
||||
configuredHooks string
|
||||
subCmd string
|
||||
expectedMatch string
|
||||
expectedOk bool
|
||||
}{
|
||||
{
|
||||
doc: "empty config",
|
||||
configuredHooks: "",
|
||||
subCmd: "build",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
{
|
||||
doc: "exact match",
|
||||
configuredHooks: "build",
|
||||
subCmd: "build",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "prefix match",
|
||||
configuredHooks: "image",
|
||||
subCmd: "image ls",
|
||||
expectedMatch: "image",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "comma-separated match",
|
||||
configuredHooks: "pull,build,push",
|
||||
subCmd: "build",
|
||||
expectedMatch: "build",
|
||||
expectedOk: true,
|
||||
},
|
||||
{
|
||||
doc: "no match",
|
||||
configuredHooks: "pull,push",
|
||||
subCmd: "build",
|
||||
expectedMatch: "",
|
||||
expectedOk: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
match, ok := matchHookConfig(tc.configuredHooks, tc.subCmd)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
})
|
||||
match, ok := pluginMatch(tc.pluginConfig, tc.commandString)
|
||||
assert.Equal(t, ok, tc.expectedOk)
|
||||
assert.Equal(t, match, tc.expectedMatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,87 +141,3 @@ func TestAppendNextSteps(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"hooks": "build"},
|
||||
}
|
||||
provider := &fakeConfigProvider{cfg: cfg}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// Should not panic with empty error message (success case)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
||||
|
||||
// Should not panic with non-empty error message (failure case)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
||||
}
|
||||
|
||||
func TestRunPluginHooksErrorHooks(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"error-hooks": "build"},
|
||||
}
|
||||
provider := &fakeConfigProvider{cfg: cfg}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// Should not panic — error-hooks with error message
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
|
||||
|
||||
// Should not panic — error-hooks with no error (should be skipped)
|
||||
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"nonexistent": {"error-hooks": "build"},
|
||||
}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
// On success, error-hooks should not match, so the plugin
|
||||
// binary is never looked up and no results are returned.
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
context.Background(), cfg, root, sub,
|
||||
"build", map[string]string{}, "some error",
|
||||
)
|
||||
assert.Check(t, is.Len(result, 0))
|
||||
}
|
||||
|
||||
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
|
||||
cfg := configfile.New("")
|
||||
cfg.Plugins = map[string]map[string]string{
|
||||
"test-plugin": {"hooks": "build"},
|
||||
}
|
||||
root := &cobra.Command{Use: "docker"}
|
||||
sub := &cobra.Command{Use: "build"}
|
||||
root.AddCommand(sub)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // cancel immediately
|
||||
|
||||
result := invokeAndCollectHooks(
|
||||
ctx, cfg, root, sub,
|
||||
"build", map[string]string{}, "exit status 1",
|
||||
)
|
||||
assert.Check(t, is.Nil(result))
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -14,12 +13,21 @@ import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||
// used to originally invoke the docker CLI when executing a
|
||||
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||
// the plugin to re-execute the original CLI.
|
||||
//
|
||||
// Deprecated: use [metadata.ReexecEnvvar]. This alias will be removed in the next release.
|
||||
ReexecEnvvar = metadata.ReexecEnvvar
|
||||
)
|
||||
|
||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||
type errPluginNotFound string
|
||||
|
||||
@@ -29,6 +37,13 @@ func (e errPluginNotFound) Error() string {
|
||||
return "Error: No such CLI plugin: " + string(e)
|
||||
}
|
||||
|
||||
// IsNotFound is true if the given error is due to a plugin not being found.
|
||||
//
|
||||
// Deprecated: use [errdefs.IsNotFound].
|
||||
func IsNotFound(err error) bool {
|
||||
return errdefs.IsNotFound(err)
|
||||
}
|
||||
|
||||
// getPluginDirs returns the platform-specific locations to search for plugins
|
||||
// in order of preference.
|
||||
//
|
||||
@@ -59,17 +74,9 @@ func addPluginCandidatesFromDir(res map[string][]string, d string) {
|
||||
return
|
||||
}
|
||||
for _, dentry := range dentries {
|
||||
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
||||
case os.ModeSymlink:
|
||||
if !debug.IsEnabled() {
|
||||
// Skip broken symlinks unless debug is enabled. With debug
|
||||
// enabled, this will print a warning in "docker info".
|
||||
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
case 0:
|
||||
// Regular file, keep going
|
||||
switch dentry.Type() & os.ModeType { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list
|
||||
case 0, os.ModeSymlink:
|
||||
// Regular file or symlink, keep going
|
||||
default:
|
||||
// Something else, ignore.
|
||||
continue
|
||||
@@ -172,14 +179,14 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
|
||||
}
|
||||
|
||||
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conflicts.
|
||||
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
|
||||
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow.
|
||||
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
|
||||
// This uses the full original args, not the args which may
|
||||
// have been provided by cobra to our caller. This is because
|
||||
// they lack e.g. global options which we must propagate here.
|
||||
args := os.Args[1:]
|
||||
if !isValidPluginName(name) {
|
||||
if !pluginNameRe.MatchString(name) {
|
||||
// We treat this as "not found" so that callers will
|
||||
// fallback to their "invalid" command path.
|
||||
return nil, errPluginNotFound(name)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"plugins3-target", // Will be referenced as a symlink from below
|
||||
fs.WithFile("docker-plugin1", ""),
|
||||
fs.WithDir("ignored3"),
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored
|
||||
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
|
||||
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
|
||||
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
|
||||
),
|
||||
@@ -72,6 +72,9 @@ func TestListPluginCandidates(t *testing.T) {
|
||||
"hardlink2": {
|
||||
dir.Join("plugins2", "docker-hardlink2"),
|
||||
},
|
||||
"brokensymlink": {
|
||||
dir.Join("plugins3", "docker-brokensymlink"),
|
||||
},
|
||||
"symlinked": {
|
||||
dir.Join("plugins3", "docker-symlinked"),
|
||||
},
|
||||
@@ -89,7 +92,7 @@ func TestListPluginCandidatesEmpty(t *testing.T) {
|
||||
// Regression test for https://github.com/docker/cli/issues/5643.
|
||||
// Check that inaccessible directories that come before accessible ones are ignored
|
||||
// and do not prevent the latter from being processed.
|
||||
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
|
||||
func TestListPluginCandidatesInaccesibleDir(t *testing.T) {
|
||||
dir := fs.NewDir(t, t.Name(),
|
||||
fs.WithDir("no-perm", fs.WithMode(0)),
|
||||
fs.WithDir("plugins",
|
||||
@@ -177,8 +180,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,
|
||||
|
||||
@@ -16,5 +16,6 @@ import (
|
||||
//
|
||||
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
|
||||
var defaultSystemPluginDirs = []string{
|
||||
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamePrefix is the prefix required on all plugin binary names
|
||||
//
|
||||
// Deprecated: use [metadata.NamePrefix]. This alias will be removed in a future release.
|
||||
NamePrefix = metadata.NamePrefix
|
||||
|
||||
// MetadataSubcommandName is the name of the plugin subcommand
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
//
|
||||
// Deprecated: use [metadata.MetadataSubcommandName]. This alias will be removed in a future release.
|
||||
MetadataSubcommandName = metadata.MetadataSubcommandName
|
||||
|
||||
// HookSubcommandName is the name of the plugin subcommand
|
||||
// which must be implemented by plugins declaring support
|
||||
// for hooks in their metadata.
|
||||
//
|
||||
// Deprecated: use [metadata.HookSubcommandName]. This alias will be removed in a future release.
|
||||
HookSubcommandName = metadata.HookSubcommandName
|
||||
)
|
||||
|
||||
// Metadata provided by the plugin.
|
||||
//
|
||||
// Deprecated: use [metadata.Metadata]. This alias will be removed in a future release.
|
||||
type Metadata = metadata.Metadata
|
||||
@@ -9,14 +9,15 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli-plugins/hooks"
|
||||
"github.com/docker/cli/cli-plugins/metadata"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
|
||||
|
||||
// Plugin represents a potential plugin with all it's metadata.
|
||||
type Plugin struct {
|
||||
metadata.Metadata
|
||||
@@ -84,8 +85,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
}
|
||||
|
||||
// Now apply the candidate tests, so these update p.Err.
|
||||
if !isValidPluginName(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat)
|
||||
if !pluginNameRe.MatchString(p.Name) {
|
||||
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -117,8 +118,8 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
p.Err = wrapAsPluginError(err, "invalid metadata")
|
||||
return p, nil
|
||||
}
|
||||
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil {
|
||||
p.Err = &pluginError{cause: err}
|
||||
if p.Metadata.SchemaVersion != "0.1.0" {
|
||||
p.Err = newPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
|
||||
return p, nil
|
||||
}
|
||||
if p.Metadata.Vendor == "" {
|
||||
@@ -128,34 +129,9 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
|
||||
//
|
||||
// The current schema-version is "0.1.0", but we don't want to break compatibility
|
||||
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
|
||||
//
|
||||
// Note that CLI versions before 28.4.1 may not support these versions as they were
|
||||
// hard-coded to only accept "0.1.0".
|
||||
func validateSchemaVersion(version string) error {
|
||||
if version == "0.1.0" {
|
||||
return nil
|
||||
}
|
||||
if version == "" {
|
||||
return errors.New("plugin SchemaVersion version cannot be empty")
|
||||
}
|
||||
major, _, ok := strings.Cut(version, ".")
|
||||
majorVersion, err := strconv.Atoi(major)
|
||||
if !ok || err != nil {
|
||||
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
|
||||
}
|
||||
if majorVersion > 1 {
|
||||
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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,37 +140,10 @@ 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
|
||||
}
|
||||
|
||||
// pluginNameFormat is used as part of errors for invalid plugin-names.
|
||||
// We should consider making this less technical ("must start with "a-z",
|
||||
// and only consist of lowercase alphanumeric characters").
|
||||
const pluginNameFormat = `^[a-z][a-z0-9]*$`
|
||||
|
||||
func isValidPluginName(s string) bool {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
// first character must be a-z
|
||||
if c := s[0]; c < 'a' || c > 'z' {
|
||||
return false
|
||||
}
|
||||
// followed by a-z or 0-9
|
||||
for i := 1; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return hookCmdOutput, nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,4 @@ type Metadata struct {
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
// Hidden hides the plugin in completion and help message output.
|
||||
Hidden bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
@@ -80,23 +80,19 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should
|
||||
// be called from the plugin's "main()" function. It initializes a new
|
||||
// [command.DockerCli] instance with the given options before calling
|
||||
// makeCmd to construct the plugin command, then invokes the plugin command
|
||||
// using [RunPlugin].
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata) {
|
||||
otel.SetErrorHandler(debug.OTELErrorHandler)
|
||||
|
||||
dockerCLI, err := command.NewDockerCli(ops...)
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, err)
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
plugin := makeCmd(dockerCLI)
|
||||
plugin := makeCmd(dockerCli)
|
||||
|
||||
if err := RunPlugin(dockerCLI, plugin, meta); err != nil {
|
||||
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
||||
var stErr cli.StatusError
|
||||
if errors.As(err, &stErr) {
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
@@ -104,10 +100,10 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops .
|
||||
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere.
|
||||
stErr.StatusCode = 1
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr)
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), stErr)
|
||||
os.Exit(stErr.StatusCode)
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), err)
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +135,7 @@ func withPluginClientConn(name string) command.CLIOption {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
|
||||
apiClient, err := client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -168,11 +164,6 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "",
|
||||
},
|
||||
}
|
||||
|
||||
// Disable file-completion by default. Most commands and flags should not
|
||||
// complete with filenames.
|
||||
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
|
||||
|
||||
opts, _ := cli.SetupPluginRootCommand(cmd)
|
||||
|
||||
cmd.SetIn(dockerCli.In())
|
||||
|
||||
@@ -93,7 +93,7 @@ func (pl *PluginServer) Addr() net.Addr {
|
||||
// Close ensures that the server is no longer accepting new connections and
|
||||
// closes all existing connections. Existing connections will receive [io.EOF].
|
||||
//
|
||||
// The error value is that of the underlying [net.Listener.Close] call.
|
||||
// The error value is that of the underlying [net.Listner.Close] call.
|
||||
func (pl *PluginServer) Close() error {
|
||||
if pl == nil {
|
||||
return nil
|
||||
|
||||
@@ -47,16 +47,16 @@ func TestPluginServer(t *testing.T) {
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("expected EOF error, got: %v", err)
|
||||
t.Fatalf("exepcted EOF error, got: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
}
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
+35
-7
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/term"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -166,6 +167,31 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
|
||||
return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
|
||||
}
|
||||
|
||||
// VisitAll will traverse all commands from the root.
|
||||
//
|
||||
// Deprecated: this utility was only used internally and will be removed in the next release.
|
||||
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
visitAll(root, fn)
|
||||
}
|
||||
|
||||
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
|
||||
for _, cmd := range root.Commands() {
|
||||
visitAll(cmd, fn)
|
||||
}
|
||||
fn(root)
|
||||
}
|
||||
|
||||
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
|
||||
// commands within the tree rooted at cmd.
|
||||
//
|
||||
// Deprecated: this utility was only used internally and will be removed in the next release.
|
||||
func DisableFlagsInUseLine(cmd *cobra.Command) {
|
||||
visitAll(cmd, func(ccmd *cobra.Command) {
|
||||
// do not add a `[flags]` to the end of the usage line.
|
||||
ccmd.DisableFlagsInUseLine = true
|
||||
})
|
||||
}
|
||||
|
||||
var helpCommand = &cobra.Command{
|
||||
Use: "help [command]",
|
||||
Short: "Help about the command",
|
||||
@@ -174,7 +200,7 @@ var helpCommand = &cobra.Command{
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cmd, args, e := c.Root().Find(args)
|
||||
if cmd == nil || e != nil || len(args) > 0 {
|
||||
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
|
||||
}
|
||||
helpFunc := cmd.HelpFunc()
|
||||
helpFunc(cmd, args)
|
||||
@@ -250,12 +276,11 @@ func commandAliases(cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
parentPath = cmd.Parent().CommandPath() + " "
|
||||
}
|
||||
var aliases strings.Builder
|
||||
aliases.WriteString(cmd.CommandPath())
|
||||
aliases := cmd.CommandPath()
|
||||
for _, alias := range cmd.Aliases {
|
||||
aliases.WriteString(", " + parentPath + alias)
|
||||
aliases += ", " + parentPath + alias
|
||||
}
|
||||
return aliases.String()
|
||||
return aliases
|
||||
}
|
||||
|
||||
func topCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
@@ -351,10 +376,13 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if invalidPluginReason(sub) != "" {
|
||||
if isPlugin(sub) {
|
||||
if invalidPluginReason(sub) == "" {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) {
|
||||
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,33 +56,6 @@ func TestInvalidPlugin(t *testing.T) {
|
||||
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||
}
|
||||
|
||||
func TestHiddenPlugin(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub1 := &cobra.Command{
|
||||
Use: "sub1",
|
||||
Hidden: true,
|
||||
Annotations: map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
|
||||
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
|
||||
sub2 := &cobra.Command{
|
||||
Use: "sub2",
|
||||
Annotations: map[string]string{
|
||||
metadata.CommandAnnotationPlugin: "true",
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {},
|
||||
}
|
||||
|
||||
root.AddCommand(sub1, sub2)
|
||||
sub1.AddCommand(sub1sub1, sub1sub2)
|
||||
|
||||
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
|
||||
}
|
||||
|
||||
func TestCommandAliases(t *testing.T) {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
|
||||
|
||||
@@ -3,17 +3,18 @@ package builder
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
|
||||
builderPruneFunc func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
if c.builderPruneFunc != nil {
|
||||
return c.builderPruneFunc(ctx, opts)
|
||||
}
|
||||
return client.BuildCachePruneResult{}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
+12
-11
@@ -6,17 +6,15 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newBuilderCommand)
|
||||
commands.Register(func(c command.Cli) *cobra.Command {
|
||||
return newBakeStubCommand(c)
|
||||
})
|
||||
// NewBuilderCommand returns a cobra command for `builder` subcommands
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newBuilderCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newBuilderCommand returns a cobra command for `builder` subcommands
|
||||
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "builder",
|
||||
@@ -24,8 +22,6 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
Annotations: map[string]string{"version": "1.31"},
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newPruneCommand(dockerCLI),
|
||||
@@ -36,10 +32,16 @@ func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// NewBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
|
||||
// This command is a placeholder / stub that is dynamically replaced by an
|
||||
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
|
||||
// installed).
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return newBakeStubCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "bake [OPTIONS] [TARGET...]",
|
||||
@@ -52,6 +54,5 @@ func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
|
||||
"aliases": "docker buildx bake",
|
||||
"version": "1.31",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,25 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/system/pruner"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the prune command to run as part of "docker system prune"
|
||||
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
keepStorage opts.MemBytes
|
||||
}
|
||||
|
||||
type pruneOptions struct {
|
||||
force bool
|
||||
all bool
|
||||
filter opts.FilterOpt
|
||||
reservedSpace opts.MemBytes
|
||||
// NewPruneCommand returns a new cobra prune command for images
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
|
||||
return newPruneCommand(dockerCli)
|
||||
}
|
||||
|
||||
// newPruneCommand returns a new cobra prune command for images
|
||||
@@ -49,16 +48,15 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
|
||||
return nil
|
||||
},
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
Annotations: map[string]string{"version": "1.39"},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
|
||||
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
|
||||
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
|
||||
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache")
|
||||
flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -69,7 +67,8 @@ const (
|
||||
)
|
||||
|
||||
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
|
||||
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value())
|
||||
pruneFilters := options.filter.Value()
|
||||
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
|
||||
|
||||
warning := normalWarning
|
||||
if options.all {
|
||||
@@ -85,15 +84,15 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{
|
||||
All: options.all,
|
||||
ReservedSpace: options.reservedSpace.Value(),
|
||||
Filters: pruneFilters,
|
||||
report, err := dockerCli.Client().BuildCachePrune(ctx, build.CachePruneOptions{
|
||||
All: options.all,
|
||||
KeepStorage: options.keepStorage.Value(), // FIXME(thaJeztah): rewrite to use new options; see https://github.com/moby/moby/pull/48720
|
||||
Filters: pruneFilters,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
report := resp.Report
|
||||
|
||||
if len(report.CachesDeleted) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Deleted build cache objects:\n")
|
||||
@@ -111,22 +110,7 @@ type cancelledErr struct{ error }
|
||||
|
||||
func (cancelledErr) Cancelled() {}
|
||||
|
||||
// pruneFn prunes the build cache for use in "docker system prune" and
|
||||
// returns the amount of space reclaimed and a detailed output string.
|
||||
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
|
||||
if !options.Confirmed {
|
||||
// Dry-run: perform validation and produce confirmation before pruning.
|
||||
var confirmMsg string
|
||||
if options.All {
|
||||
confirmMsg = "all build cache"
|
||||
} else {
|
||||
confirmMsg = "unused build cache"
|
||||
}
|
||||
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
|
||||
}
|
||||
return runPrune(ctx, dockerCLI, pruneOptions{
|
||||
force: true,
|
||||
all: options.All,
|
||||
filter: options.Filter,
|
||||
})
|
||||
// CachePrune executes a prune command for build cache
|
||||
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
|
||||
return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
)
|
||||
|
||||
func TestBuilderPromptTermination(t *testing.T) {
|
||||
@@ -15,8 +15,8 @@ func TestBuilderPromptTermination(t *testing.T) {
|
||||
t.Cleanup(cancel)
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
|
||||
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
builderPruneFunc: func(ctx context.Context, opts build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||
return nil, errors.New("fakeClient builderPruneFunc should not be called")
|
||||
},
|
||||
})
|
||||
cmd := newPruneCommand(cli)
|
||||
|
||||
@@ -3,33 +3,34 @@ package checkpoint
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
|
||||
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
|
||||
if cli.checkpointCreateFunc != nil {
|
||||
return cli.checkpointCreateFunc(container, options)
|
||||
}
|
||||
return client.CheckpointCreateResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
|
||||
if cli.checkpointDeleteFunc != nil {
|
||||
return cli.checkpointDeleteFunc(container, options)
|
||||
}
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
if cli.checkpointListFunc != nil {
|
||||
return cli.checkpointListFunc(container, options)
|
||||
}
|
||||
return client.CheckpointListResult{}, nil
|
||||
return []checkpoint.Summary{}, nil
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ package checkpoint
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newCheckpointCommand)
|
||||
// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCheckpointCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
|
||||
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "checkpoint",
|
||||
@@ -23,7 +24,6 @@ func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"ostype": "linux",
|
||||
"version": "1.25",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newCreateCommand(dockerCLI),
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ type createOptions struct {
|
||||
leaveRunning bool
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newCreateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts createOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -27,10 +27,9 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.container = args[0]
|
||||
opts.checkpoint = args[1]
|
||||
return runCreate(cmd.Context(), dockerCLI, opts)
|
||||
return runCreate(cmd.Context(), dockerCli, opts)
|
||||
},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -41,7 +40,7 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error {
|
||||
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{
|
||||
err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
|
||||
CheckpointID: opts.checkpoint,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
Exit: !opts.leaveRunning,
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func TestCheckpointCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error)
|
||||
checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -29,8 +29,8 @@ func TestCheckpointCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo")
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
return errors.New("error creating checkpoint for container foo")
|
||||
},
|
||||
expectedError: "error creating checkpoint for container foo",
|
||||
},
|
||||
@@ -59,12 +59,12 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
leaveRunning := strconv.FormatBool(tc)
|
||||
t.Run("leave-running="+leaveRunning, func(t *testing.T) {
|
||||
var actualContainerName string
|
||||
var actualOptions client.CheckpointCreateOptions
|
||||
var actualOptions checkpoint.CreateOptions
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) {
|
||||
checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
|
||||
actualContainerName = container
|
||||
actualOptions = options
|
||||
return client.CheckpointCreateResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(cli)
|
||||
@@ -75,7 +75,7 @@ func TestCheckpointCreateWithOptions(t *testing.T) {
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal(actualContainerName, containerName))
|
||||
expected := client.CheckpointCreateOptions{
|
||||
expected := checkpoint.CreateOptions{
|
||||
CheckpointID: checkpointName,
|
||||
CheckpointDir: checkpointDir,
|
||||
Exit: !tc,
|
||||
|
||||
@@ -2,7 +2,7 @@ package checkpoint
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -10,6 +10,13 @@ const (
|
||||
checkpointNameHeader = "CHECKPOINT NAME"
|
||||
)
|
||||
|
||||
// NewFormat returns a format for use with a checkpoint Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewFormat(source string) formatter.Format {
|
||||
return newFormat(source)
|
||||
}
|
||||
|
||||
// newFormat returns a format for use with a checkpointContext.
|
||||
func newFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
@@ -18,23 +25,24 @@ func newFormat(source string) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes formatted checkpoints using the Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func FormatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
return formatWrite(fmtCtx, checkpoints)
|
||||
}
|
||||
|
||||
// formatWrite writes formatted checkpoints using the Context
|
||||
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error {
|
||||
cpContext := &checkpointContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, cp := range checkpoints {
|
||||
if err := format(&checkpointContext{c: cp}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return fmtCtx.Write(newCheckpointContext(), render)
|
||||
}
|
||||
|
||||
type checkpointContext struct {
|
||||
@@ -42,6 +50,14 @@ type checkpointContext struct {
|
||||
c checkpoint.Summary
|
||||
}
|
||||
|
||||
func newCheckpointContext() *checkpointContext {
|
||||
cpCtx := checkpointContext{}
|
||||
cpCtx.Header = formatter.SubHeaderContext{
|
||||
"Name": checkpointNameHeader,
|
||||
}
|
||||
return &cpCtx
|
||||
}
|
||||
|
||||
func (c *checkpointContext) MarshalJSON() ([]byte, error) {
|
||||
return formatter.MarshalJSON(c)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ type listOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newListCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts listOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -24,10 +24,9 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "List checkpoints for a container",
|
||||
Args: cli.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), dockerCLI, args[0], opts)
|
||||
return runList(cmd.Context(), dockerCli, args[0], opts)
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCli, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -36,8 +35,8 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error {
|
||||
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{
|
||||
func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
|
||||
checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -45,8 +44,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, container string, opts
|
||||
}
|
||||
|
||||
cpCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Output: dockerCli.Out(),
|
||||
Format: newFormat(formatter.TableFormatKey),
|
||||
}
|
||||
return formatWrite(cpCtx, checkpoints.Items)
|
||||
return formatWrite(cpCtx, checkpoints)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/checkpoint"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@@ -16,7 +15,7 @@ import (
|
||||
func TestCheckpointListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error)
|
||||
checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -29,8 +28,8 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo")
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
return []checkpoint.Summary{}, errors.New("error getting checkpoints for container foo")
|
||||
},
|
||||
expectedError: "error getting checkpoints for container foo",
|
||||
},
|
||||
@@ -51,19 +50,17 @@ func TestCheckpointListErrors(t *testing.T) {
|
||||
func TestCheckpointListWithOptions(t *testing.T) {
|
||||
var containerID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) {
|
||||
checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
|
||||
containerID = container
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointListResult{
|
||||
Items: []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
},
|
||||
return []checkpoint.Summary{
|
||||
{Name: "checkpoint-foo"},
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
cmd := newListCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("/dir/foo", checkpointDir))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package checkpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -11,7 +13,7 @@ type removeOptions struct {
|
||||
checkpointDir string
|
||||
}
|
||||
|
||||
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var opts removeOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -20,14 +22,8 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Short: "Remove a checkpoint",
|
||||
Args: cli.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
containerID, checkpointID := args[0], args[1]
|
||||
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
|
||||
CheckpointID: checkpointID,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
return err
|
||||
return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -35,3 +31,10 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
|
||||
return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
|
||||
CheckpointID: checkpointID,
|
||||
CheckpointDir: opts.checkpointDir,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/checkpoint"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error)
|
||||
checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -27,8 +27,8 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint")
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
return errors.New("error deleting checkpoint")
|
||||
},
|
||||
expectedError: "error deleting checkpoint",
|
||||
},
|
||||
@@ -49,16 +49,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
|
||||
func TestCheckpointRemoveWithOptions(t *testing.T) {
|
||||
var containerID, checkpointID, checkpointDir string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) {
|
||||
checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
|
||||
containerID = container
|
||||
checkpointID = options.CheckpointID
|
||||
checkpointDir = options.CheckpointDir
|
||||
return client.CheckpointRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newRemoveCommand(cli)
|
||||
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
|
||||
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo"))
|
||||
cmd.Flags().Set("checkpoint-dir", "/dir/foo")
|
||||
assert.NilError(t, cmd.Execute())
|
||||
assert.Check(t, is.Equal("container-foo", containerID))
|
||||
assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
|
||||
|
||||
+57
-38
@@ -1,11 +1,10 @@
|
||||
// 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.23
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -24,8 +23,11 @@ import (
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/version"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/build"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types/build"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -43,9 +45,12 @@ type Cli interface {
|
||||
Client() client.APIClient
|
||||
Streams
|
||||
SetIn(in *streams.In)
|
||||
Apply(ops ...CLIOption) error
|
||||
config.Provider
|
||||
ServerInfo() ServerInfo
|
||||
DefaultVersion() string
|
||||
CurrentVersion() string
|
||||
ContentTrustEnabled() bool
|
||||
BuildKitEnabled() (bool, error)
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
@@ -60,12 +65,12 @@ type Cli interface {
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
options *cliflags.ClientOptions
|
||||
clientOpts []client.Opt
|
||||
in *streams.In
|
||||
out *streams.Out
|
||||
err *streams.Out
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
contentTrust bool
|
||||
contextStore store.Store
|
||||
currentContext string
|
||||
init sync.Once
|
||||
@@ -83,12 +88,17 @@ type DockerCli struct {
|
||||
enableGlobalMeter, enableGlobalTracer bool
|
||||
}
|
||||
|
||||
// DefaultVersion returns [api.DefaultVersion].
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
|
||||
// CurrentVersion returns the API version currently negotiated, or the default
|
||||
// version otherwise.
|
||||
func (cli *DockerCli) CurrentVersion() string {
|
||||
_ = cli.initialize()
|
||||
if cli.client == nil {
|
||||
return client.MaxAPIVersion
|
||||
return api.DefaultVersion
|
||||
}
|
||||
return cli.client.ClientVersion()
|
||||
}
|
||||
@@ -147,13 +157,19 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
return cli.serverInfo
|
||||
}
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// BuildKitEnabled returns buildkit is enabled or not.
|
||||
func (cli *DockerCli) BuildKitEnabled() (bool, error) {
|
||||
// use DOCKER_BUILDKIT env var value if set and not empty
|
||||
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
|
||||
enabled, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err)
|
||||
return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
|
||||
}
|
||||
return enabled, nil
|
||||
}
|
||||
@@ -253,7 +269,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -290,17 +306,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
|
||||
contextStore := &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), storeConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return resolveDefaultContext(opts, storeConfig)
|
||||
return ResolveDefaultContext(opts, storeConfig)
|
||||
},
|
||||
}
|
||||
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err)
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
||||
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||
}
|
||||
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
opts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -308,15 +324,8 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
if len(configFile.HTTPHeaders) > 0 {
|
||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||
}
|
||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withCustomHeaders != nil {
|
||||
opts = append(opts, withCustomHeaders)
|
||||
}
|
||||
opts = append(opts, extraOpts...)
|
||||
return client.New(opts...)
|
||||
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
|
||||
@@ -377,21 +386,24 @@ func (cli *DockerCli) initializeFromClient() {
|
||||
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
|
||||
defer cancel()
|
||||
|
||||
ping, err := cli.client.Ping(ctx, client.PingOptions{
|
||||
NegotiateAPIVersion: true,
|
||||
ForceNegotiate: true,
|
||||
})
|
||||
ping, err := cli.client.Ping(ctx)
|
||||
if err != nil {
|
||||
// Default to true if we fail to connect to daemon
|
||||
cli.serverInfo = ServerInfo{HasExperimental: true}
|
||||
|
||||
if ping.APIVersion != "" {
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cli.serverInfo = ServerInfo{
|
||||
HasExperimental: ping.Experimental,
|
||||
OSType: ping.OSType,
|
||||
BuildkitVersion: ping.BuilderVersion,
|
||||
SwarmStatus: ping.SwarmStatus,
|
||||
}
|
||||
cli.client.NegotiateAPIVersionPing(ping)
|
||||
}
|
||||
|
||||
// ContextStore returns the ContextStore
|
||||
@@ -529,11 +541,11 @@ func (cli *DockerCli) initialize() error {
|
||||
cli.init.Do(func() {
|
||||
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
|
||||
if cli.initErr != nil {
|
||||
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr)
|
||||
cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil {
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -545,6 +557,16 @@ func (cli *DockerCli) initialize() error {
|
||||
return cli.initErr
|
||||
}
|
||||
|
||||
// Apply all the operation on the cli
|
||||
func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
// server
|
||||
type ServerInfo struct {
|
||||
@@ -559,26 +581,23 @@ type ServerInfo struct {
|
||||
// in the ping response, or if an error occurred, in which case the client
|
||||
// should use other ways to get the current swarm status, such as the /swarm
|
||||
// endpoint.
|
||||
SwarmStatus *client.SwarmStatus
|
||||
SwarmStatus *swarm.Status
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with all operators applied on it.
|
||||
// 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{
|
||||
WithContentTrustFromEnv(),
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
)
|
||||
}
|
||||
ops = append(defaultOps, ops...)
|
||||
|
||||
cli := &DockerCli{baseCtx: context.Background()}
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cli.Apply(ops...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
@@ -590,11 +609,11 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
||||
case 1:
|
||||
return dopts.ParseHost(defaultToTLS, hosts[0])
|
||||
default:
|
||||
return "", errors.New("specify only one -H")
|
||||
return "", errors.New("Specify only one -H")
|
||||
}
|
||||
}
|
||||
|
||||
// UserAgent returns the default user agent string used for making API requests.
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
func UserAgent() string {
|
||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||
}
|
||||
|
||||
+73
-70
@@ -3,16 +3,16 @@ package command
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CLIOption is a functional argument to apply options to a [DockerCli]. These
|
||||
@@ -75,6 +75,28 @@ func WithErrorStream(err io.Writer) CLIOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func WithContentTrustFromEnv() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = false
|
||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||
if t, err := strconv.ParseBool(e); t || err != nil {
|
||||
// treat any other value as true
|
||||
cli.contentTrust = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
func WithContentTrust(enabled bool) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = enabled
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
|
||||
func WithDefaultContextStoreConfig() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
@@ -104,16 +126,6 @@ func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error))
|
||||
}
|
||||
}
|
||||
|
||||
// WithAPIClientOptions configures additional [client.Opt] to use when
|
||||
// initializing the API client. These options have no effect if a custom
|
||||
// client is set (through [WithAPIClient] or [WithInitializeClient]).
|
||||
func WithAPIClientOptions(c ...client.Opt) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.clientOpts = append(cli.clientOpts, c...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// envOverrideHTTPHeaders is the name of the environment-variable that can be
|
||||
// used to set custom HTTP headers to be sent by the client. This environment
|
||||
// variable is the equivalent to the HttpHeaders field in the configuration
|
||||
@@ -168,70 +180,61 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
||||
// override headers with the same name).
|
||||
//
|
||||
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
|
||||
func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
func withCustomHeadersFromEnv() client.Opt {
|
||||
return func(apiClient *client.Client) error {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return nil, invalidParameter(fmt.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil, nil
|
||||
}
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env), nil
|
||||
}
|
||||
if k == "" {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
// WithUserAgent configures the User-Agent string for cli HTTP requests.
|
||||
func WithUserAgent(userAgent string) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
if userAgent == "" {
|
||||
return errors.New("user agent cannot be blank")
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
cli.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent))
|
||||
return nil
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env)(apiClient)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func contentTrustEnabled(t *testing.T) bool {
|
||||
t.Helper()
|
||||
var cli DockerCli
|
||||
assert.NilError(t, WithContentTrustFromEnv()(&cli))
|
||||
return cli.contentTrust
|
||||
}
|
||||
|
||||
// NB: Do not t.Parallel() this test -- it messes with the process environment.
|
||||
func TestWithContentTrustFromEnv(t *testing.T) {
|
||||
const envvar = "DOCKER_CONTENT_TRUST"
|
||||
t.Setenv(envvar, "true")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "false")
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
t.Setenv(envvar, "invalid")
|
||||
assert.Check(t, contentTrustEnabled(t))
|
||||
os.Unsetenv(envvar)
|
||||
assert.Check(t, !contentTrustEnabled(t))
|
||||
}
|
||||
+58
-118
@@ -20,7 +20,9 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/context/store"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -33,7 +35,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
@@ -46,7 +48,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), slug+host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
@@ -70,7 +72,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
|
||||
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
|
||||
@@ -79,7 +81,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
|
||||
"My-Header": "Custom-Value",
|
||||
"User-Agent": UserAgent(),
|
||||
}
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
@@ -105,7 +107,7 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
apiClient, err := NewAPIClientFromFlags(opts, configFile)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, apiClient.DaemonHost(), host)
|
||||
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
|
||||
assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
|
||||
|
||||
expectedHeaders := http.Header{
|
||||
"One": []string{"one-value"},
|
||||
@@ -114,14 +116,14 @@ func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
|
||||
"Four": []string{"four-value-override"},
|
||||
"User-Agent": []string{UserAgent()},
|
||||
}
|
||||
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
|
||||
_, err = apiClient.Ping(context.Background())
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, expectedHeaders)
|
||||
}
|
||||
|
||||
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
const customVersion = "v3.3"
|
||||
const expectedVersion = "3.3"
|
||||
const customVersion = "v3.3.3"
|
||||
const expectedVersion = "3.3.3"
|
||||
t.Setenv("DOCKER_API_VERSION", customVersion)
|
||||
t.Setenv("DOCKER_HOST", ":2375")
|
||||
|
||||
@@ -134,55 +136,51 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
pingFunc func() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
version string
|
||||
negotiated bool
|
||||
}
|
||||
|
||||
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) {
|
||||
res, err := c.pingFunc()
|
||||
if options.NegotiateAPIVersion {
|
||||
if res.APIVersion != "" {
|
||||
if c.negotiated || options.ForceNegotiate {
|
||||
c.negotiated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return res, err
|
||||
func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
|
||||
return c.pingFunc()
|
||||
}
|
||||
|
||||
func (c *fakeClient) ClientVersion() string {
|
||||
return c.version
|
||||
}
|
||||
|
||||
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
|
||||
c.negotiated = true
|
||||
}
|
||||
|
||||
func TestInitializeFromClient(t *testing.T) {
|
||||
const defaultVersion = "v1.55"
|
||||
|
||||
testcases := []struct {
|
||||
doc string
|
||||
pingFunc func() (client.PingResult, error)
|
||||
pingFunc func() (types.Ping, error)
|
||||
expectedServer ServerInfo
|
||||
negotiated bool
|
||||
}{
|
||||
{
|
||||
doc: "successful ping",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
|
||||
negotiated: true,
|
||||
},
|
||||
{
|
||||
doc: "failed ping, no API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
},
|
||||
{
|
||||
doc: "failed ping, with API version",
|
||||
pingFunc: func() (client.PingResult, error) {
|
||||
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed")
|
||||
pingFunc: func() (types.Ping, error) {
|
||||
return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
|
||||
},
|
||||
expectedServer: ServerInfo{HasExperimental: true},
|
||||
negotiated: true,
|
||||
@@ -208,82 +206,58 @@ 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.Background(), 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:
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("some input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
// Test default operations and also overriding default ones
|
||||
cli, err := NewDockerCli(WithInputStream(io.NopCloser(strings.NewReader("some input"))))
|
||||
assert.NilError(t, err)
|
||||
// Check streams are initialized
|
||||
assert.Check(t, cli.In() != nil)
|
||||
@@ -293,6 +267,19 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "some input")
|
||||
|
||||
// Apply can modify a dockerCli after construction
|
||||
outbuf := bytes.NewBuffer(nil)
|
||||
errbuf := bytes.NewBuffer(nil)
|
||||
err = cli.Apply(
|
||||
WithInputStream(io.NopCloser(strings.NewReader("input"))),
|
||||
WithOutputStream(outbuf),
|
||||
WithErrorStream(errbuf),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
// Check input stream
|
||||
inputStream, err = io.ReadAll(cli.In())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, string(inputStream), "input")
|
||||
// Check output stream
|
||||
_, err = fmt.Fprint(cli.Out(), "output")
|
||||
assert.NilError(t, err)
|
||||
@@ -310,9 +297,9 @@ func TestNewDockerCliAndOperators(t *testing.T) {
|
||||
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
|
||||
cli, err := NewDockerCli()
|
||||
assert.NilError(t, err)
|
||||
apiClient, err := client.New()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient)))
|
||||
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
|
||||
return client.NewClientWithOpts()
|
||||
})))
|
||||
assert.Check(t, cli.ContextStore() != nil)
|
||||
}
|
||||
|
||||
@@ -376,7 +363,6 @@ func TestSetGoDebug(t *testing.T) {
|
||||
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
|
||||
})
|
||||
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
|
||||
t.Setenv("GODEBUG", "")
|
||||
meta := store.Metadata{
|
||||
Metadata: DockerContext{
|
||||
AdditionalFields: map[string]any{
|
||||
@@ -388,49 +374,3 @@ func TestSetGoDebug(t *testing.T) {
|
||||
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
|
||||
var received string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.UserAgent()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithUserAgent("fake-agent/0.0.1"),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
cli.currentContext = DefaultContextName
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
|
||||
var received string
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
received = r.UserAgent()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
|
||||
opts := &flags.ClientOptions{Hosts: []string{host}}
|
||||
|
||||
cli, err := NewDockerCli(
|
||||
WithAPIClientOptions(client.WithUserAgent("fake-agent/0.0.1")),
|
||||
)
|
||||
assert.NilError(t, err)
|
||||
cli.currentContext = DefaultContextName
|
||||
cli.options = opts
|
||||
cli.configFile = &configfile.ConfigFile{}
|
||||
|
||||
_, err = cli.Client().Ping(t.Context(), client.PingOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, received, "fake-agent/0.0.1")
|
||||
}
|
||||
|
||||
@@ -1,30 +1,168 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
_ "github.com/docker/cli/cli/command/builder"
|
||||
_ "github.com/docker/cli/cli/command/checkpoint"
|
||||
_ "github.com/docker/cli/cli/command/config"
|
||||
_ "github.com/docker/cli/cli/command/container"
|
||||
_ "github.com/docker/cli/cli/command/context"
|
||||
_ "github.com/docker/cli/cli/command/image"
|
||||
_ "github.com/docker/cli/cli/command/manifest"
|
||||
_ "github.com/docker/cli/cli/command/network"
|
||||
_ "github.com/docker/cli/cli/command/node"
|
||||
_ "github.com/docker/cli/cli/command/plugin"
|
||||
_ "github.com/docker/cli/cli/command/registry"
|
||||
_ "github.com/docker/cli/cli/command/secret"
|
||||
_ "github.com/docker/cli/cli/command/service"
|
||||
_ "github.com/docker/cli/cli/command/stack"
|
||||
_ "github.com/docker/cli/cli/command/swarm"
|
||||
_ "github.com/docker/cli/cli/command/system"
|
||||
_ "github.com/docker/cli/cli/command/volume"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/docker/cli/cli/command/builder"
|
||||
"github.com/docker/cli/cli/command/checkpoint"
|
||||
"github.com/docker/cli/cli/command/config"
|
||||
"github.com/docker/cli/cli/command/container"
|
||||
"github.com/docker/cli/cli/command/context"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/command/manifest"
|
||||
"github.com/docker/cli/cli/command/network"
|
||||
"github.com/docker/cli/cli/command/node"
|
||||
"github.com/docker/cli/cli/command/plugin"
|
||||
"github.com/docker/cli/cli/command/registry"
|
||||
"github.com/docker/cli/cli/command/secret"
|
||||
"github.com/docker/cli/cli/command/service"
|
||||
"github.com/docker/cli/cli/command/stack"
|
||||
"github.com/docker/cli/cli/command/swarm"
|
||||
"github.com/docker/cli/cli/command/system"
|
||||
"github.com/docker/cli/cli/command/trust"
|
||||
"github.com/docker/cli/cli/command/volume"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) {
|
||||
for _, c := range commands.Commands() {
|
||||
cmd.AddCommand(c(dockerCLI))
|
||||
// AddCommands adds all the commands from cli/command to the root command
|
||||
func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
cmd.AddCommand(
|
||||
// commonly used shorthands
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewRunCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewExecCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewPsCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewBuildCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewPullCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewPushCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewImagesCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewLoginCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewLogoutCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
registry.NewSearchCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewVersionCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewInfoCommand(dockerCli),
|
||||
|
||||
// management commands
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
builder.NewBakeStubCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
builder.NewBuilderCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
checkpoint.NewCheckpointCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
container.NewContainerCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
context.NewContextCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
image.NewImageCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
manifest.NewManifestCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
network.NewNetworkCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
plugin.NewPluginCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
system.NewSystemCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
trust.NewTrustCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// orchestration (swarm) commands
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
config.NewConfigCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
node.NewNodeCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
secret.NewSecretCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
service.NewServiceCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
stack.NewStackCommand(dockerCli),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
swarm.NewSwarmCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewAttachCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCommitCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCopyCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewCreateCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewDiffCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewExportCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewKillCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewLogsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewPauseCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewPortCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRenameCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRestartCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewRmCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStartCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStatsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewStopCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewTopCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewUnpauseCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewUpdateCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(container.NewWaitCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewHistoryCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewImportCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewLoadCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewRemoveCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
|
||||
hide(system.NewInspectCommand(dockerCli)),
|
||||
)
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
|
||||
// these legacy commands (such as `docker ps`, `docker exec`, etc)
|
||||
// will not be shown in output console.
|
||||
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
|
||||
return cmd
|
||||
}
|
||||
cmdCopy := *cmd
|
||||
cmdCopy.Hidden = true
|
||||
cmdCopy.Aliases = []string{}
|
||||
return &cmdCopy
|
||||
}
|
||||
|
||||
@@ -4,13 +4,16 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// APIClientProvider provides a method to get a [client.APIClient], initializing
|
||||
// APIClientProvider provides a method to get an [client.APIClient], initializing
|
||||
// it if needed.
|
||||
//
|
||||
// It's a smaller interface than [command.Cli], and used in situations where an
|
||||
@@ -22,61 +25,28 @@ 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
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, img := range res.Items {
|
||||
for _, img := range list {
|
||||
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) {
|
||||
if limit > 0 && len(args) >= limit {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
baseNameCounts := make(map[string]int)
|
||||
for _, img := range res.Items {
|
||||
names = append(names, img.RepoTags...)
|
||||
for _, tag := range img.RepoTags {
|
||||
ref, err := reference.ParseNormalizedNamed(tag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
baseNameCounts[reference.FamiliarName(ref)]++
|
||||
}
|
||||
}
|
||||
for baseName, count := range baseNameCounts {
|
||||
if count > 1 {
|
||||
names = append(names, baseName)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().ContainerList(cmd.Context(), container.ListOptions{
|
||||
All: all,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -86,7 +56,7 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
|
||||
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
|
||||
|
||||
var names []string
|
||||
for _, ctr := range res.Items {
|
||||
for _, ctr := range list {
|
||||
skip := false
|
||||
for _, fn := range filters {
|
||||
if fn != nil && !fn(ctr) {
|
||||
@@ -100,46 +70,40 @@ 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) {
|
||||
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, vol := range res.Items {
|
||||
for _, vol := range list.Volumes {
|
||||
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) {
|
||||
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, nw := range res.Items {
|
||||
for _, nw := range list {
|
||||
names = append(names, nw.Name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// EnvVarNames offers completion for environment-variable names. This
|
||||
@@ -155,52 +119,33 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
|
||||
// export MY_VAR=hello
|
||||
// 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) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
|
||||
envs := os.Environ()
|
||||
names = make([]string, 0, len(envs))
|
||||
for _, env := range envs {
|
||||
name, _, _ := strings.Cut(env, "=")
|
||||
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],
|
||||
// which indicates to let the shell perform its default behavior after
|
||||
// completions have been provided.
|
||||
func FileNames() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveDefault
|
||||
}
|
||||
|
||||
// NoComplete is used for commands where there's no relevant completion.
|
||||
//
|
||||
// Deprecated: use [cobra.NoFileCompletions]. This function will be removed in the next release.
|
||||
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
var commonPlatforms = []string{
|
||||
@@ -240,43 +185,6 @@ var commonPlatforms = []string{
|
||||
// - we currently exclude architectures that may have unofficial builds,
|
||||
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
|
||||
// ppc64 (non-le) to prevent confusion.
|
||||
func Platforms() cobra.CompletionFunc {
|
||||
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
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
|
||||
}
|
||||
func Platforms(_ *cobra.Command, _ []string, _ string) (platforms []string, _ cobra.ShellCompDirective) {
|
||||
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/image"
|
||||
"github.com/moby/moby/api/types/network"
|
||||
"github.com/moby/moby/api/types/volume"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -28,38 +30,38 @@ func (c fakeCLI) Client() client.APIClient {
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
|
||||
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
|
||||
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
|
||||
containerListFunc func(options container.ListOptions) ([]container.Summary, error)
|
||||
imageListFunc func(options image.ListOptions) ([]image.Summary, error)
|
||||
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
|
||||
volumeListFunc func(filter filters.Args) (volume.ListResponse, error)
|
||||
}
|
||||
|
||||
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
func (c *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
if c.containerListFunc != nil {
|
||||
return c.containerListFunc(ctx, options)
|
||||
return c.containerListFunc(options)
|
||||
}
|
||||
return client.ContainerListResult{}, nil
|
||||
return []container.Summary{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
|
||||
func (c *fakeClient) ImageList(_ context.Context, options image.ListOptions) ([]image.Summary, error) {
|
||||
if c.imageListFunc != nil {
|
||||
return c.imageListFunc(ctx, options)
|
||||
return c.imageListFunc(options)
|
||||
}
|
||||
return client.ImageListResult{}, nil
|
||||
return []image.Summary{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
func (c *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
if c.networkListFunc != nil {
|
||||
return c.networkListFunc(ctx, options)
|
||||
}
|
||||
return client.NetworkListResult{}, nil
|
||||
return []network.Inspect{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
func (c *fakeClient) VolumeList(_ context.Context, options volume.ListOptions) (volume.ListResponse, error) {
|
||||
if c.volumeListFunc != nil {
|
||||
return c.volumeListFunc(ctx, options)
|
||||
return c.volumeListFunc(options.Filters)
|
||||
}
|
||||
return client.VolumeListResult{}, nil
|
||||
return volume.ListResponse{}, nil
|
||||
}
|
||||
|
||||
func TestCompleteContainerNames(t *testing.T) {
|
||||
@@ -69,7 +71,7 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
filters []func(container.Summary) bool
|
||||
containers []container.Summary
|
||||
expOut []string
|
||||
expOpts client.ContainerListOptions
|
||||
expOpts container.ListOptions
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
@@ -84,8 +86,8 @@ 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"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@@ -97,8 +99,8 @@ 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"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@@ -107,7 +109,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,12 +119,12 @@ 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"}},
|
||||
},
|
||||
expOut: []string{"container-b"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@@ -133,12 +135,12 @@ 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"}},
|
||||
},
|
||||
expOut: []string{"container-a"},
|
||||
expOpts: client.ContainerListOptions{All: true},
|
||||
expOpts: container.ListOptions{All: true},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
@@ -153,12 +155,12 @@ func TestCompleteContainerNames(t *testing.T) {
|
||||
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
|
||||
}
|
||||
comp := ContainerNames(fakeCLI{&fakeClient{
|
||||
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
|
||||
containerListFunc: func(opts container.ListOptions) ([]container.Summary, error) {
|
||||
assert.Check(t, is.DeepEqual(opts, tc.expOpts, cmpopts.IgnoreUnexported(container.ListOptions{}, filters.Args{})))
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ContainerListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.ContainerListResult{Items: tc.containers}, nil
|
||||
return tc.containers, nil
|
||||
},
|
||||
}}, tc.showAll, tc.filters...)
|
||||
|
||||
@@ -174,7 +176,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
|
||||
"ENV_A": "hello-a",
|
||||
"ENV_B": "hello-b",
|
||||
})
|
||||
values, directives := EnvVarNames()(nil, nil, "")
|
||||
values, directives := EnvVarNames(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
|
||||
sort.Strings(values)
|
||||
@@ -183,7 +185,7 @@ func TestCompleteEnvVarNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCompleteFileNames(t *testing.T) {
|
||||
values, directives := FileNames()(nil, nil, "")
|
||||
values, directives := FileNames(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
|
||||
assert.Check(t, is.Len(values, 0))
|
||||
}
|
||||
@@ -196,18 +198,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
|
||||
@@ -238,11 +228,11 @@ func TestCompleteImageNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := ImageNames(fakeCLI{&fakeClient{
|
||||
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
|
||||
imageListFunc: func(options image.ListOptions) ([]image.Summary, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.ImageListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.ImageListResult{Items: tc.images}, nil
|
||||
return tc.images, nil
|
||||
},
|
||||
}}, -1)
|
||||
|
||||
@@ -267,24 +257,9 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
{
|
||||
doc: "with results",
|
||||
networks: []network.Summary{
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-c",
|
||||
Name: "network-c",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-b",
|
||||
Name: "network-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
Network: network.Network{
|
||||
ID: "nw-a",
|
||||
Name: "network-a",
|
||||
},
|
||||
},
|
||||
{ID: "nw-c", Name: "network-c"},
|
||||
{ID: "nw-b", Name: "network-b"},
|
||||
{ID: "nw-a", Name: "network-a"},
|
||||
},
|
||||
expOut: []string{"network-c", "network-b", "network-a"},
|
||||
expDirective: cobra.ShellCompDirectiveNoFileComp,
|
||||
@@ -298,11 +273,11 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := NetworkNames(fakeCLI{&fakeClient{
|
||||
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
|
||||
networkListFunc: func(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.NetworkListResult{}, errors.New("some error occurred")
|
||||
return nil, errors.New("some error occurred")
|
||||
}
|
||||
return client.NetworkListResult{Items: tc.networks}, nil
|
||||
return tc.networks, nil
|
||||
},
|
||||
}})
|
||||
|
||||
@@ -314,7 +289,7 @@ func TestCompleteNetworkNames(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCompletePlatforms(t *testing.T) {
|
||||
values, directives := Platforms()(nil, nil, "")
|
||||
values, directives := Platforms(nil, nil, "")
|
||||
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
|
||||
assert.Check(t, is.DeepEqual(values, commonPlatforms))
|
||||
}
|
||||
@@ -322,7 +297,7 @@ func TestCompletePlatforms(t *testing.T) {
|
||||
func TestCompleteVolumeNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
volumes []volume.Volume
|
||||
volumes []*volume.Volume
|
||||
expOut []string
|
||||
expDirective cobra.ShellCompDirective
|
||||
}{
|
||||
@@ -332,7 +307,7 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
},
|
||||
{
|
||||
doc: "with results",
|
||||
volumes: []volume.Volume{
|
||||
volumes: []*volume.Volume{
|
||||
{Name: "volume-c"},
|
||||
{Name: "volume-b"},
|
||||
{Name: "volume-a"},
|
||||
@@ -349,11 +324,11 @@ func TestCompleteVolumeNames(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
comp := VolumeNames(fakeCLI{&fakeClient{
|
||||
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
|
||||
volumeListFunc: func(filter filters.Args) (volume.ListResponse, error) {
|
||||
if tc.expDirective == cobra.ShellCompDirectiveError {
|
||||
return client.VolumeListResult{}, errors.New("some error occurred")
|
||||
return volume.ListResponse{}, errors.New("some error occurred")
|
||||
}
|
||||
return client.VolumeListResult{Items: tc.volumes}, nil
|
||||
return volume.ListResponse{Volumes: tc.volumes}, nil
|
||||
},
|
||||
}})
|
||||
|
||||
@@ -363,23 +338,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"}))
|
||||
}
|
||||
|
||||
@@ -3,41 +3,42 @@ package config
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
configRemoveFunc func(string) error
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if c.configCreateFunc != nil {
|
||||
return c.configCreateFunc(ctx, options)
|
||||
return c.configCreateFunc(ctx, spec)
|
||||
}
|
||||
return client.ConfigCreateResult{}, nil
|
||||
return swarm.ConfigCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
|
||||
if c.configInspectFunc != nil {
|
||||
return c.configInspectFunc(ctx, id, options)
|
||||
return c.configInspectFunc(ctx, id)
|
||||
}
|
||||
return client.ConfigInspectResult{}, nil
|
||||
return swarm.Config{}, nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
func (c *fakeClient) ConfigList(ctx context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
if c.configListFunc != nil {
|
||||
return c.configListFunc(ctx, options)
|
||||
}
|
||||
return client.ConfigListResult{}, nil
|
||||
return []swarm.Config{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
|
||||
if c.configRemoveFunc != nil {
|
||||
return c.configRemoveFunc(ctx, name, options)
|
||||
return c.configRemoveFunc(name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newConfigCommand)
|
||||
// NewConfigCommand returns a cobra command for `config` subcommands
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newConfigCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newConfigCommand returns a cobra command for `config` subcommands
|
||||
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config",
|
||||
@@ -24,7 +25,6 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"version": "1.30",
|
||||
"swarm": "manager",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newConfigListCommand(dockerCLI),
|
||||
@@ -38,12 +38,12 @@ func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
// completeNames offers completion for swarm configs
|
||||
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{})
|
||||
list, err := dockerCLI.Client().ConfigList(cmd.Context(), swarm.ConfigListOptions{})
|
||||
if err != nil {
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
var names []string
|
||||
for _, config := range res.Items {
|
||||
for _, config := range list {
|
||||
names = append(names, config.ID)
|
||||
}
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
|
||||
@@ -2,19 +2,28 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/moby/sys/sequential"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// CreateOptions specifies some options that are used when creating a config.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type CreateOptions struct {
|
||||
Name string
|
||||
TemplateDriver string
|
||||
File string
|
||||
Labels opts.ListOpts
|
||||
}
|
||||
|
||||
// createOptions specifies some options that are used when creating a config.
|
||||
type createOptions struct {
|
||||
name string
|
||||
@@ -37,24 +46,7 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
createOpts.file = args[1]
|
||||
return runCreate(cmd.Context(), dockerCLI, createOpts)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch len(args) {
|
||||
case 0:
|
||||
// No completion for the first argument, which is the name for
|
||||
// the new config, but if a non-empty name is given, we return
|
||||
// it as completion to allow "tab"-ing to the next completion.
|
||||
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
|
||||
case 1:
|
||||
// Second argument is either "-" or a file to load.
|
||||
//
|
||||
// TODO(thaJeztah): provide completion for "-".
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
|
||||
default:
|
||||
// Command only accepts two arguments.
|
||||
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.VarP(&createOpts.labels, "label", "l", "Config labels")
|
||||
@@ -64,13 +56,25 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigCreate creates a config with the given options.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigCreate(ctx context.Context, dockerCLI command.Cli, options CreateOptions) error {
|
||||
return runCreate(ctx, dockerCLI, createOptions{
|
||||
name: options.Name,
|
||||
templateDriver: options.TemplateDriver,
|
||||
file: options.File,
|
||||
labels: options.Labels,
|
||||
})
|
||||
}
|
||||
|
||||
// runCreate creates a config with the given options.
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
configData, err := readConfigData(dockerCLI.In(), options.file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading content from %q: %v", options.file, err)
|
||||
return errors.Errorf("Error reading content from %q: %v", options.file, err)
|
||||
}
|
||||
|
||||
spec := swarm.ConfigSpec{
|
||||
@@ -85,9 +89,7 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions
|
||||
Name: options.templateDriver,
|
||||
}
|
||||
}
|
||||
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{
|
||||
Spec: spec,
|
||||
})
|
||||
r, err := apiClient.ConfigCreate(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
@@ -24,7 +23,7 @@ const configDataFile = "config-create-with-name.golden"
|
||||
func TestConfigCreateErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error)
|
||||
configCreateFunc func(context.Context, swarm.ConfigSpec) (swarm.ConfigCreateResponse, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -37,8 +36,8 @@ func TestConfigCreateErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"name", filepath.Join("testdata", configDataFile)},
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
return client.ConfigCreateResult{}, errors.New("error creating config")
|
||||
configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
return swarm.ConfigCreateResponse{}, errors.New("error creating config")
|
||||
},
|
||||
expectedError: "error creating config",
|
||||
},
|
||||
@@ -62,15 +61,15 @@ func TestConfigCreateWithName(t *testing.T) {
|
||||
const name = "config-with-name"
|
||||
var actual []byte
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
actual = options.Spec.Data
|
||||
actual = spec.Data
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -101,13 +100,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
|
||||
}
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if !reflect.DeepEqual(options.Spec, expected) {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec)
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if !reflect.DeepEqual(spec, expected) {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected %+v, got %+v", expected, spec)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -127,17 +126,17 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
|
||||
const name = "config-with-template-driver"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) {
|
||||
if options.Spec.Name != name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name)
|
||||
configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||
if spec.Name != name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected name %q, got %q", name, spec.Name)
|
||||
}
|
||||
|
||||
if options.Spec.Templating.Name != expectedDriver.Name {
|
||||
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels)
|
||||
if spec.Templating.Name != expectedDriver.Name {
|
||||
return swarm.ConfigCreateResponse{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
|
||||
}
|
||||
|
||||
return client.ConfigCreateResult{
|
||||
ID: "ID-" + options.Spec.Name,
|
||||
return swarm.ConfigCreateResponse{
|
||||
ID: "ID-" + spec.Name,
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
// 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"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/cli/command/inspect"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,6 +29,13 @@ Data:
|
||||
{{.Data}}`
|
||||
)
|
||||
|
||||
// NewFormat returns a Format for rendering using a config Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewFormat(source string, quiet bool) formatter.Format {
|
||||
return newFormat(source, quiet)
|
||||
}
|
||||
|
||||
// newFormat returns a Format for rendering using a configContext.
|
||||
func newFormat(source string, quiet bool) formatter.Format {
|
||||
switch source {
|
||||
@@ -48,28 +50,38 @@ func newFormat(source string, quiet bool) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// FormatWrite writes the context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func FormatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
|
||||
return formatWrite(fmtCtx, configs)
|
||||
}
|
||||
|
||||
// formatWrite writes the context
|
||||
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error {
|
||||
cCtx := &configContext{
|
||||
HeaderContext: formatter.HeaderContext{
|
||||
Header: formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
},
|
||||
},
|
||||
}
|
||||
return fmtCtx.Write(cCtx, func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs.Items {
|
||||
func formatWrite(fmtCtx formatter.Context, configs []swarm.Config) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, config := range configs {
|
||||
configCtx := &configContext{c: config}
|
||||
if err := format(configCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return fmtCtx.Write(newConfigContext(), render)
|
||||
}
|
||||
|
||||
func newConfigContext() *configContext {
|
||||
cCtx := &configContext{}
|
||||
|
||||
cCtx.Header = formatter.SubHeaderContext{
|
||||
"ID": configIDHeader,
|
||||
"Name": formatter.NameHeader,
|
||||
"CreatedAt": configCreatedHeader,
|
||||
"UpdatedAt": configUpdatedHeader,
|
||||
"Labels": formatter.LabelsHeader,
|
||||
}
|
||||
return cCtx
|
||||
}
|
||||
|
||||
type configContext struct {
|
||||
@@ -106,7 +118,6 @@ func (c *configContext) Labels() string {
|
||||
for k, v := range mapLabels {
|
||||
joinLabels = append(joinLabels, k+"="+v)
|
||||
}
|
||||
slices.Sort(joinLabels)
|
||||
return strings.Join(joinLabels, ",")
|
||||
}
|
||||
|
||||
@@ -117,12 +128,19 @@ func (c *configContext) Label(name string) string {
|
||||
return c.c.Spec.Annotations.Labels[name]
|
||||
}
|
||||
|
||||
// InspectFormatWrite renders the context for a list of configs
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func InspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
return inspectFormatWrite(fmtCtx, refs, getRef)
|
||||
}
|
||||
|
||||
// inspectFormatWrite renders the context for a list of configs
|
||||
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
|
||||
if fmtCtx.Format != configInspectPrettyTemplate {
|
||||
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef)
|
||||
}
|
||||
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error {
|
||||
render := func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, ref := range refs {
|
||||
configI, _, err := getRef(ref)
|
||||
if err != nil {
|
||||
@@ -137,7 +155,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return fmtCtx.Write(&configInspectContext{}, render)
|
||||
}
|
||||
|
||||
type configInspectContext struct {
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -49,25 +48,23 @@ id_rsa
|
||||
},
|
||||
}
|
||||
|
||||
res := client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
{
|
||||
ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||
},
|
||||
configs := []swarm.Config{
|
||||
{
|
||||
ID: "1",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
|
||||
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(string(tc.context.Format), func(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
tc.context.Output = &out
|
||||
if err := formatWrite(tc.context, res); err != nil {
|
||||
if err := formatWrite(tc.context, configs); err != nil {
|
||||
assert.ErrorContains(t, err, tc.expected)
|
||||
} else {
|
||||
assert.Equal(t, out.String(), tc.expected)
|
||||
|
||||
@@ -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.23
|
||||
|
||||
package config
|
||||
|
||||
@@ -12,10 +12,18 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// InspectOptions contains options for the docker config inspect command.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type InspectOptions struct {
|
||||
Names []string
|
||||
Format string
|
||||
Pretty bool
|
||||
}
|
||||
|
||||
// inspectOptions contains options for the docker config inspect command.
|
||||
type inspectOptions struct {
|
||||
names []string
|
||||
@@ -33,8 +41,9 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
opts.names = args
|
||||
return runInspect(cmd.Context(), dockerCLI, opts)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp)
|
||||
@@ -42,6 +51,17 @@ func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigInspect inspects the given Swarm config.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigInspect(ctx context.Context, dockerCLI command.Cli, opts InspectOptions) error {
|
||||
return runInspect(ctx, dockerCLI, inspectOptions{
|
||||
names: opts.Names,
|
||||
format: opts.Format,
|
||||
pretty: opts.Pretty,
|
||||
})
|
||||
}
|
||||
|
||||
// runInspect inspects the given Swarm config.
|
||||
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
@@ -51,8 +71,7 @@ func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions)
|
||||
}
|
||||
|
||||
getRef := func(id string) (any, []byte, error) {
|
||||
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{})
|
||||
return res.Config, res.Raw, err
|
||||
return apiClient.ConfigInspectWithRaw(ctx, id)
|
||||
}
|
||||
|
||||
// check if the user is trying to apply a template to the pretty format, which
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
flags map[string]string
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@@ -41,13 +41,11 @@ func TestConfigInspectErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
|
||||
if configID == "foo" {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(builders.ConfigName("foo")),
|
||||
}, nil
|
||||
return *builders.Config(builders.ConfigName("foo")), nil, nil
|
||||
}
|
||||
return client.ConfigInspectResult{}, errors.New("error while inspecting the config")
|
||||
return swarm.Config{}, nil, errors.New("error while inspecting the config")
|
||||
},
|
||||
expectedError: "error while inspecting the config",
|
||||
},
|
||||
@@ -72,34 +70,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "single-config",
|
||||
args: []string{"foo"},
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
if name != "foo" {
|
||||
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
return swarm.Config{}, nil, fmt.Errorf("invalid name, expected %s, got %s", "foo", name)
|
||||
}
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
),
|
||||
}, nil
|
||||
return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple-configs-with-labels",
|
||||
args: []string{"foo", "bar"},
|
||||
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigID("ID-"+name),
|
||||
builders.ConfigName(name),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -113,19 +102,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigInspectWithFormat(t *testing.T) {
|
||||
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
|
||||
"label1": "label-foo",
|
||||
})), nil, nil
|
||||
}
|
||||
testCases := []struct {
|
||||
name string
|
||||
format string
|
||||
args []string
|
||||
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple-template",
|
||||
@@ -155,23 +141,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
|
||||
func TestConfigInspectPretty(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error)
|
||||
configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) {
|
||||
return client.ConfigInspectResult{
|
||||
Config: *builders.Config(
|
||||
builders.ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
builders.ConfigID("configID"),
|
||||
builders.ConfigName("configName"),
|
||||
builders.ConfigCreatedAt(time.Time{}),
|
||||
builders.ConfigUpdatedAt(time.Time{}),
|
||||
builders.ConfigData([]byte("payload here")),
|
||||
),
|
||||
}, nil
|
||||
configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
|
||||
return *builders.Config(
|
||||
builders.ConfigLabels(map[string]string{
|
||||
"lbl1": "value1",
|
||||
}),
|
||||
builders.ConfigID("configID"),
|
||||
builders.ConfigName("configName"),
|
||||
builders.ConfigCreatedAt(time.Time{}),
|
||||
builders.ConfigUpdatedAt(time.Time{}),
|
||||
builders.ConfigData([]byte("payload here")),
|
||||
), []byte{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,11 +9,20 @@ import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/fvbommel/sortorder"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// ListOptions contains options for the docker config ls command.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type ListOptions struct {
|
||||
Quiet bool
|
||||
Format string
|
||||
Filter opts.FilterOpt
|
||||
}
|
||||
|
||||
// listOptions contains options for the docker config ls command.
|
||||
type listOptions struct {
|
||||
quiet bool
|
||||
@@ -32,8 +41,7 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runList(cmd.Context(), dockerCLI, listOpts)
|
||||
},
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: cobra.NoFileCompletions,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -44,11 +52,22 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// RunConfigList lists Swarm configs.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigList(ctx context.Context, dockerCLI command.Cli, options ListOptions) error {
|
||||
return runList(ctx, dockerCLI, listOptions{
|
||||
quiet: options.Quiet,
|
||||
format: options.Format,
|
||||
filter: options.Filter,
|
||||
})
|
||||
}
|
||||
|
||||
// runList lists Swarm configs.
|
||||
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()})
|
||||
configs, err := apiClient.ConfigList(ctx, swarm.ConfigListOptions{Filters: options.filter.Value()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,13 +81,13 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(res.Items, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name)
|
||||
sort.Slice(configs, func(i, j int) bool {
|
||||
return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
|
||||
})
|
||||
|
||||
configCtx := formatter.Context{
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newFormat(format, options.quiet),
|
||||
}
|
||||
return formatWrite(configCtx, res)
|
||||
return formatWrite(configCtx, configs)
|
||||
}
|
||||
|
||||
@@ -10,16 +10,16 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/golden"
|
||||
)
|
||||
|
||||
func TestConfigListErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error)
|
||||
configListFunc func(context.Context, swarm.ConfigListOptions) ([]swarm.Config, error)
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -27,8 +27,8 @@ func TestConfigListErrors(t *testing.T) {
|
||||
expectedError: "accepts no argument",
|
||||
},
|
||||
{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{}, errors.New("error listing configs")
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{}, errors.New("error listing configs")
|
||||
},
|
||||
expectedError: "error listing configs",
|
||||
},
|
||||
@@ -48,28 +48,26 @@ func TestConfigListErrors(t *testing.T) {
|
||||
|
||||
func TestConfigList(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||
builders.ConfigName("10-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||
builders.ConfigName("2-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-1-foo"),
|
||||
builders.ConfigName("1-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-10-foo"),
|
||||
builders.ConfigName("10-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-2-foo"),
|
||||
builders.ConfigName("2-foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -80,14 +78,12 @@ func TestConfigList(t *testing.T) {
|
||||
|
||||
func TestConfigListWithQuietOption(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -99,14 +95,12 @@ func TestConfigListWithQuietOption(t *testing.T) {
|
||||
|
||||
func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -120,14 +114,12 @@ func TestConfigListWithConfigFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFormat(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
},
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
|
||||
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
|
||||
"label": "label-bar",
|
||||
})),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
@@ -139,24 +131,22 @@ func TestConfigListWithFormat(t *testing.T) {
|
||||
|
||||
func TestConfigListWithFilter(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) {
|
||||
assert.Check(t, options.Filters["name"]["foo"])
|
||||
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"])
|
||||
return client.ConfigListResult{
|
||||
Items: []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-bar"),
|
||||
builders.ConfigName("bar"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
},
|
||||
configListFunc: func(_ context.Context, options swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||
assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
|
||||
assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
|
||||
return []swarm.Config{
|
||||
*builders.Config(builders.ConfigID("ID-foo"),
|
||||
builders.ConfigName("foo"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 10}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
*builders.Config(builders.ConfigID("ID-bar"),
|
||||
builders.ConfigName("bar"),
|
||||
builders.ConfigVersion(swarm.Version{Index: 11}),
|
||||
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
|
||||
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
|
||||
),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -7,10 +7,16 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// RemoveOptions contains options for the docker config rm command.
|
||||
//
|
||||
// Deprecated: this type was for internal use and will be removed in the next release.
|
||||
type RemoveOptions struct {
|
||||
Names []string
|
||||
}
|
||||
|
||||
func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm CONFIG [CONFIG...]",
|
||||
@@ -20,18 +26,26 @@ func newConfigRemoveCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRemove(cmd.Context(), dockerCLI, args)
|
||||
},
|
||||
ValidArgsFunction: completeNames(dockerCLI),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return completeNames(dockerCLI)(cmd, args, toComplete)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunConfigRemove removes the given Swarm configs.
|
||||
//
|
||||
// Deprecated: this function was for internal use and will be removed in the next release.
|
||||
func RunConfigRemove(ctx context.Context, dockerCLI command.Cli, opts RemoveOptions) error {
|
||||
return runRemove(ctx, dockerCLI, opts.Names)
|
||||
}
|
||||
|
||||
// runRemove removes the given Swarm configs.
|
||||
func runRemove(ctx context.Context, dockerCLI command.Cli, names []string) error {
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
var errs []error
|
||||
for _, name := range names {
|
||||
if _, err := apiClient.ConfigRemove(ctx, name, client.ConfigRemoveOptions{}); err != nil {
|
||||
if err := apiClient.ConfigRemove(ctx, name); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
@@ -16,7 +14,7 @@ import (
|
||||
func TestConfigRemoveErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error)
|
||||
configRemoveFunc func(string) error
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@@ -25,8 +23,8 @@ func TestConfigRemoveErrors(t *testing.T) {
|
||||
},
|
||||
{
|
||||
args: []string{"foo"},
|
||||
configRemoveFunc: func(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config")
|
||||
configRemoveFunc: func(name string) error {
|
||||
return errors.New("error removing config")
|
||||
},
|
||||
expectedError: "error removing config",
|
||||
},
|
||||
@@ -48,9 +46,9 @@ func TestConfigRemoveWithName(t *testing.T) {
|
||||
names := []string{"foo", "bar"}
|
||||
var removedConfigs []string
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
cmd := newConfigRemoveCommand(cli)
|
||||
@@ -65,12 +63,12 @@ func TestConfigRemoveContinueAfterError(t *testing.T) {
|
||||
var removedConfigs []string
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
configRemoveFunc: func(_ context.Context, name string, _ client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) {
|
||||
configRemoveFunc: func(name string) error {
|
||||
removedConfigs = append(removedConfigs, name)
|
||||
if name == "foo" {
|
||||
return client.ConfigRemoveResult{}, errors.New("error removing config: " + name)
|
||||
return errors.New("error removing config: " + name)
|
||||
}
|
||||
return client.ConfigRemoveResult{}, nil
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -23,24 +23,30 @@ type AttachOptions struct {
|
||||
}
|
||||
|
||||
func inspectContainerAndCheckState(ctx context.Context, apiClient client.APIClient, args string) (*container.InspectResponse, error) {
|
||||
c, err := apiClient.ContainerInspect(ctx, args, client.ContainerInspectOptions{})
|
||||
c, err := apiClient.ContainerInspect(ctx, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !c.Container.State.Running {
|
||||
return nil, errors.New("cannot attach to a stopped container, start it first")
|
||||
if !c.State.Running {
|
||||
return nil, errors.New("You cannot attach to a stopped container, start it first")
|
||||
}
|
||||
if c.Container.State.Paused {
|
||||
return nil, errors.New("cannot attach to a paused container, unpause it first")
|
||||
if c.State.Paused {
|
||||
return nil, errors.New("You cannot attach to a paused container, unpause it first")
|
||||
}
|
||||
if c.Container.State.Restarting {
|
||||
return nil, errors.New("cannot attach to a restarting container, wait until it is running")
|
||||
if c.State.Restarting {
|
||||
return nil, errors.New("You cannot attach to a restarting container, wait until it is running")
|
||||
}
|
||||
|
||||
return &c.Container, nil
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// NewAttachCommand creates a new cobra.Command for `docker attach`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newAttachCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newAttachCommand creates a new cobra.Command for `docker attach`
|
||||
func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts AttachOptions
|
||||
|
||||
@@ -58,7 +64,6 @@ func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false, func(ctr container.Summary) bool {
|
||||
return ctr.State != container.StatePaused
|
||||
}),
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -70,19 +75,11 @@ func newAttachCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
|
||||
// RunAttach executes an `attach` command
|
||||
func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, opts *AttachOptions) error {
|
||||
detachKeys := opts.DetachKeys
|
||||
if detachKeys == "" {
|
||||
detachKeys = dockerCLI.ConfigFile().DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(detachKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
|
||||
// request channel to wait for client
|
||||
waitCtx := context.WithoutCancel(ctx)
|
||||
waitRes := apiClient.ContainerWait(waitCtx, containerID, client.ContainerWaitOptions{})
|
||||
resultC, errC := apiClient.ContainerWait(waitCtx, containerID, "")
|
||||
|
||||
c, err := inspectContainerAndCheckState(ctx, apiClient, containerID)
|
||||
if err != nil {
|
||||
@@ -93,7 +90,12 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
return err
|
||||
}
|
||||
|
||||
options := client.ContainerAttachOptions{
|
||||
detachKeys := dockerCLI.ConfigFile().DetachKeys
|
||||
if opts.DetachKeys != "" {
|
||||
detachKeys = opts.DetachKeys
|
||||
}
|
||||
|
||||
options := container.AttachOptions{
|
||||
Stream: true,
|
||||
Stdin: !opts.NoStdin && c.Config.OpenStdin,
|
||||
Stdout: true,
|
||||
@@ -117,11 +119,11 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
defer signal.StopCatch(sigc)
|
||||
}
|
||||
|
||||
res, err := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if err != nil {
|
||||
return err
|
||||
resp, errAttach := apiClient.ContainerAttach(ctx, containerID, options)
|
||||
if errAttach != nil {
|
||||
return errAttach
|
||||
}
|
||||
defer res.HijackedResponse.Close()
|
||||
defer resp.Close()
|
||||
|
||||
// If use docker attach command to attach to a stop container, it will return
|
||||
// "You cannot attach to a stopped container" error, it's ok, but when
|
||||
@@ -145,7 +147,7 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
inputStream: in,
|
||||
outputStream: dockerCLI.Out(),
|
||||
errorStream: dockerCLI.Err(),
|
||||
resp: res.HijackedResponse,
|
||||
resp: resp,
|
||||
tty: c.Config.Tty,
|
||||
detachKeys: options.DetachKeys,
|
||||
}
|
||||
@@ -155,19 +157,19 @@ func RunAttach(ctx context.Context, dockerCLI command.Cli, containerID string, o
|
||||
return err
|
||||
}
|
||||
|
||||
return getExitStatus(waitRes)
|
||||
return getExitStatus(errC, resultC)
|
||||
}
|
||||
|
||||
func getExitStatus(waitRes client.ContainerWaitResult) error {
|
||||
func getExitStatus(errC <-chan error, resultC <-chan container.WaitResponse) error {
|
||||
select {
|
||||
case result := <-waitRes.Result:
|
||||
case result := <-resultC:
|
||||
if result.Error != nil {
|
||||
return errors.New(result.Error.Message)
|
||||
}
|
||||
if result.StatusCode != 0 {
|
||||
return cli.StatusError{StatusCode: int(result.StatusCode)}
|
||||
}
|
||||
case err := <-waitRes.Error:
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -180,7 +182,7 @@ func resizeTTY(ctx context.Context, dockerCli command.Cli, containerID string) {
|
||||
// terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially
|
||||
// resize it, then go back to normal. Without this, every attach after the first will
|
||||
// require the user to manually resize or hit enter.
|
||||
resizeTTYTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
resizeTtyTo(ctx, dockerCli.Client(), containerID, height+1, width+1, false)
|
||||
|
||||
// After the above resizing occurs, the call to MonitorTtySize below will handle resetting back
|
||||
// to the actual size.
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
@@ -17,31 +16,23 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid-detach-keys",
|
||||
args: []string{"--detach-keys", "shift-b", "5cb5bb5e4a3b"},
|
||||
expectedError: "invalid detach keys (shift-b):",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "client-stopped",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a stopped container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: false,
|
||||
},
|
||||
@@ -52,10 +43,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-paused",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a paused container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: true,
|
||||
@@ -67,10 +58,10 @@ func TestNewAttachCommandErrors(t *testing.T) {
|
||||
{
|
||||
name: "client-restarting",
|
||||
args: []string{"5cb5bb5e4a3b"},
|
||||
expectedError: "cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{
|
||||
Container: container.InspectResponse{
|
||||
expectedError: "You cannot attach to a restarting container",
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{
|
||||
ContainerJSONBase: &container.ContainerJSONBase{
|
||||
State: &container.State{
|
||||
Running: true,
|
||||
Paused: false,
|
||||
@@ -133,10 +124,7 @@ func TestGetExitStatus(t *testing.T) {
|
||||
resultC <- *testcase.result
|
||||
}
|
||||
|
||||
err := getExitStatus(client.ContainerWaitResult{
|
||||
Result: resultC,
|
||||
Error: errC,
|
||||
})
|
||||
err := getExitStatus(errC, resultC)
|
||||
|
||||
if testcase.expectedError == nil {
|
||||
assert.NilError(t, err)
|
||||
|
||||
@@ -3,244 +3,232 @@ package container
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func mockContainerExportResult(content string) client.ContainerExportResult {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
}
|
||||
|
||||
func mockContainerLogsResult(content string) client.ContainerLogsResult {
|
||||
return io.NopCloser(strings.NewReader(content))
|
||||
}
|
||||
|
||||
type fakeStreamResult struct {
|
||||
io.ReadCloser
|
||||
client.ImagePushResponse // same interface as [client.ImagePushResponse]
|
||||
}
|
||||
|
||||
func (e fakeStreamResult) Read(p []byte) (int, error) { return e.ReadCloser.Read(p) }
|
||||
func (e fakeStreamResult) Close() error { return e.ReadCloser.Close() }
|
||||
|
||||
type fakeClient struct {
|
||||
client.Client
|
||||
inspectFunc func(string) (client.ContainerInspectResult, error)
|
||||
execInspectFunc func(execID string) (client.ExecInspectResult, error)
|
||||
execCreateFunc func(containerID string, options client.ExecCreateOptions) (client.ExecCreateResult, error)
|
||||
createContainerFunc func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error)
|
||||
containerStartFunc func(containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error)
|
||||
imagePullFunc func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error)
|
||||
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)
|
||||
containerExportFunc func(string) (client.ContainerExportResult, error)
|
||||
containerExecResizeFunc func(id string, options client.ExecResizeOptions) (client.ExecResizeResult, error)
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error)
|
||||
containerRestartFunc func(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error)
|
||||
containerStopFunc func(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error)
|
||||
containerKillFunc func(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error)
|
||||
containerPruneFunc func(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error)
|
||||
containerAttachFunc func(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error)
|
||||
containerDiffFunc func(ctx context.Context, containerID string) (client.ContainerDiffResult, error)
|
||||
inspectFunc func(string) (container.InspectResponse, error)
|
||||
execInspectFunc func(execID string) (container.ExecInspect, error)
|
||||
execCreateFunc func(containerID string, options container.ExecOptions) (container.ExecCreateResponse, error)
|
||||
createContainerFunc func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string) (container.CreateResponse, error)
|
||||
containerStartFunc func(containerID string, options container.StartOptions) error
|
||||
imageCreateFunc func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error)
|
||||
infoFunc func() (system.Info, error)
|
||||
containerStatPathFunc func(containerID, path string) (container.PathStat, error)
|
||||
containerCopyFromFunc func(containerID, srcPath string) (io.ReadCloser, container.PathStat, error)
|
||||
logFunc func(string, container.LogsOptions) (io.ReadCloser, error)
|
||||
waitFunc func(string) (<-chan container.WaitResponse, <-chan error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
containerExportFunc func(string) (io.ReadCloser, error)
|
||||
containerExecResizeFunc func(id string, options container.ResizeOptions) error
|
||||
containerRemoveFunc func(ctx context.Context, containerID string, options container.RemoveOptions) error
|
||||
containerRestartFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerStopFunc func(ctx context.Context, containerID string, options container.StopOptions) error
|
||||
containerKillFunc func(ctx context.Context, containerID, signal string) error
|
||||
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
|
||||
containerAttachFunc func(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error)
|
||||
containerDiffFunc func(ctx context.Context, containerID string) ([]container.FilesystemChange, error)
|
||||
containerRenameFunc func(ctx context.Context, oldName, newName string) error
|
||||
containerCommitFunc func(ctx context.Context, container string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error)
|
||||
containerPauseFunc func(ctx context.Context, container string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error)
|
||||
containerCommitFunc func(ctx context.Context, container string, options container.CommitOptions) (container.CommitResponse, error)
|
||||
containerPauseFunc func(ctx context.Context, container string) error
|
||||
Version string
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
func (f *fakeClient) ContainerList(_ context.Context, options container.ListOptions) ([]container.Summary, error) {
|
||||
if f.containerListFunc != nil {
|
||||
return f.containerListFunc(options)
|
||||
}
|
||||
return client.ContainerListResult{}, nil
|
||||
return []container.Summary{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string, _ client.ContainerInspectOptions) (client.ContainerInspectResult, error) {
|
||||
func (f *fakeClient) ContainerInspect(_ context.Context, containerID string) (container.InspectResponse, error) {
|
||||
if f.inspectFunc != nil {
|
||||
return f.inspectFunc(containerID)
|
||||
}
|
||||
return client.ContainerInspectResult{}, nil
|
||||
return container.InspectResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecCreate(_ context.Context, containerID string, config client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
func (f *fakeClient) ContainerExecCreate(_ context.Context, containerID string, config container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
if f.execCreateFunc != nil {
|
||||
return f.execCreateFunc(containerID, config)
|
||||
}
|
||||
return client.ExecCreateResult{}, nil
|
||||
return container.ExecCreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecInspect(_ context.Context, execID string, _ client.ExecInspectOptions) (client.ExecInspectResult, error) {
|
||||
func (f *fakeClient) ContainerExecInspect(_ context.Context, execID string) (container.ExecInspect, error) {
|
||||
if f.execInspectFunc != nil {
|
||||
return f.execInspectFunc(execID)
|
||||
}
|
||||
return client.ExecInspectResult{}, nil
|
||||
return container.ExecInspect{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) ExecStart(context.Context, string, client.ExecStartOptions) (client.ExecStartResult, error) {
|
||||
return client.ExecStartResult{}, nil
|
||||
func (*fakeClient) ContainerExecStart(context.Context, string, container.ExecStartOptions) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCreate(_ context.Context, options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
func (f *fakeClient) ContainerCreate(
|
||||
_ context.Context,
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
if f.createContainerFunc != nil {
|
||||
return f.createContainerFunc(options)
|
||||
return f.createContainerFunc(config, hostConfig, networkingConfig, platform, containerName)
|
||||
}
|
||||
return client.ContainerCreateResult{}, nil
|
||||
return container.CreateResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options client.ContainerRemoveOptions) (client.ContainerRemoveResult, error) {
|
||||
func (f *fakeClient) ContainerRemove(ctx context.Context, containerID string, options container.RemoveOptions) error {
|
||||
if f.containerRemoveFunc != nil {
|
||||
return f.containerRemoveFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerRemoveResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ImagePull(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
if f.imagePullFunc != nil {
|
||||
return f.imagePullFunc(ctx, parentReference, options)
|
||||
func (f *fakeClient) ImageCreate(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
if f.imageCreateFunc != nil {
|
||||
return f.imageCreateFunc(ctx, parentReference, options)
|
||||
}
|
||||
return fakeStreamResult{}, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) Info(context.Context, client.InfoOptions) (client.SystemInfoResult, error) {
|
||||
func (f *fakeClient) Info(_ context.Context) (system.Info, error) {
|
||||
if f.infoFunc != nil {
|
||||
return f.infoFunc()
|
||||
}
|
||||
return client.SystemInfoResult{}, nil
|
||||
return system.Info{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID string, options client.ContainerStatPathOptions) (client.ContainerStatPathResult, error) {
|
||||
func (f *fakeClient) ContainerStatPath(_ context.Context, containerID, path string) (container.PathStat, error) {
|
||||
if f.containerStatPathFunc != nil {
|
||||
return f.containerStatPathFunc(containerID, options.Path)
|
||||
return f.containerStatPathFunc(containerID, path)
|
||||
}
|
||||
return client.ContainerStatPathResult{}, nil
|
||||
return container.PathStat{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, options client.CopyFromContainerOptions) (client.CopyFromContainerResult, error) {
|
||||
func (f *fakeClient) CopyFromContainer(_ context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
if f.containerCopyFromFunc != nil {
|
||||
return f.containerCopyFromFunc(containerID, options.SourcePath)
|
||||
return f.containerCopyFromFunc(containerID, srcPath)
|
||||
}
|
||||
return client.CopyFromContainerResult{}, nil
|
||||
return nil, container.PathStat{}, 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) {
|
||||
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options container.LogsOptions) (io.ReadCloser, error) {
|
||||
if f.logFunc != nil {
|
||||
return f.logFunc(containerID, options)
|
||||
}
|
||||
return http.NoBody, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ClientVersion() string {
|
||||
return f.Version
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ client.ContainerWaitOptions) client.ContainerWaitResult {
|
||||
func (f *fakeClient) ContainerWait(_ context.Context, containerID string, _ container.WaitCondition) (<-chan container.WaitResponse, <-chan error) {
|
||||
if f.waitFunc != nil {
|
||||
return f.waitFunc(containerID)
|
||||
}
|
||||
return client.ContainerWaitResult{}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options client.ContainerStartOptions) (client.ContainerStartResult, error) {
|
||||
func (f *fakeClient) ContainerStart(_ context.Context, containerID string, options container.StartOptions) error {
|
||||
if f.containerStartFunc != nil {
|
||||
return f.containerStartFunc(containerID, options)
|
||||
}
|
||||
return client.ContainerStartResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string, _ client.ContainerExportOptions) (client.ContainerExportResult, error) {
|
||||
func (f *fakeClient) ContainerExport(_ context.Context, containerID string) (io.ReadCloser, error) {
|
||||
if f.containerExportFunc != nil {
|
||||
return f.containerExportFunc(containerID)
|
||||
}
|
||||
return http.NoBody, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ExecResize(_ context.Context, id string, options client.ExecResizeOptions) (client.ExecResizeResult, error) {
|
||||
func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options container.ResizeOptions) error {
|
||||
if f.containerExecResizeFunc != nil {
|
||||
return f.containerExecResizeFunc(id, options)
|
||||
}
|
||||
return client.ExecResizeResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID string, options client.ContainerKillOptions) (client.ContainerKillResult, error) {
|
||||
func (f *fakeClient) ContainerKill(ctx context.Context, containerID, signal string) error {
|
||||
if f.containerKillFunc != nil {
|
||||
return f.containerKillFunc(ctx, containerID, options)
|
||||
return f.containerKillFunc(ctx, containerID, signal)
|
||||
}
|
||||
return client.ContainerKillResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPrune(ctx context.Context, options client.ContainerPruneOptions) (client.ContainerPruneResult, error) {
|
||||
func (f *fakeClient) ContainersPrune(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error) {
|
||||
if f.containerPruneFunc != nil {
|
||||
return f.containerPruneFunc(ctx, options)
|
||||
return f.containerPruneFunc(ctx, pruneFilters)
|
||||
}
|
||||
return client.ContainerPruneResult{}, nil
|
||||
return container.PruneReport{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options client.ContainerRestartOptions) (client.ContainerRestartResult, error) {
|
||||
func (f *fakeClient) ContainerRestart(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerRestartFunc != nil {
|
||||
return f.containerRestartFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerRestartResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options client.ContainerStopOptions) (client.ContainerStopResult, error) {
|
||||
func (f *fakeClient) ContainerStop(ctx context.Context, containerID string, options container.StopOptions) error {
|
||||
if f.containerStopFunc != nil {
|
||||
return f.containerStopFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerStopResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options client.ContainerAttachOptions) (client.ContainerAttachResult, error) {
|
||||
func (f *fakeClient) ContainerAttach(ctx context.Context, containerID string, options container.AttachOptions) (types.HijackedResponse, error) {
|
||||
if f.containerAttachFunc != nil {
|
||||
return f.containerAttachFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerAttachResult{}, nil
|
||||
return types.HijackedResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string, _ client.ContainerDiffOptions) (client.ContainerDiffResult, error) {
|
||||
func (f *fakeClient) ContainerDiff(ctx context.Context, containerID string) ([]container.FilesystemChange, error) {
|
||||
if f.containerDiffFunc != nil {
|
||||
return f.containerDiffFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return client.ContainerDiffResult{}, nil
|
||||
return []container.FilesystemChange{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName string, options client.ContainerRenameOptions) (client.ContainerRenameResult, error) {
|
||||
func (f *fakeClient) ContainerRename(ctx context.Context, oldName, newName string) error {
|
||||
if f.containerRenameFunc != nil {
|
||||
return client.ContainerRenameResult{}, f.containerRenameFunc(ctx, oldName, options.NewName)
|
||||
return f.containerRenameFunc(ctx, oldName, newName)
|
||||
}
|
||||
|
||||
return client.ContainerRenameResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
func (f *fakeClient) ContainerCommit(ctx context.Context, containerID string, options container.CommitOptions) (container.CommitResponse, error) {
|
||||
if f.containerCommitFunc != nil {
|
||||
return f.containerCommitFunc(ctx, containerID, options)
|
||||
}
|
||||
return client.ContainerCommitResult{}, nil
|
||||
return container.CommitResponse{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string, options client.ContainerPauseOptions) (client.ContainerPauseResult, error) {
|
||||
func (f *fakeClient) ContainerPause(ctx context.Context, containerID string) error {
|
||||
if f.containerPauseFunc != nil {
|
||||
return f.containerPauseFunc(ctx, containerID, options)
|
||||
return f.containerPauseFunc(ctx, containerID)
|
||||
}
|
||||
|
||||
return client.ContainerPauseResult{}, nil
|
||||
}
|
||||
|
||||
func (*fakeClient) Ping(_ context.Context, _ client.PingOptions) (client.PingResult, error) {
|
||||
return client.PingResult{}, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,46 +3,22 @@ package container
|
||||
import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/commands"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
commands.Register(newRunCommand)
|
||||
commands.Register(newExecCommand)
|
||||
commands.Register(newPsCommand)
|
||||
commands.Register(newContainerCommand)
|
||||
commands.RegisterLegacy(newAttachCommand)
|
||||
commands.RegisterLegacy(newCommitCommand)
|
||||
commands.RegisterLegacy(newCopyCommand)
|
||||
commands.RegisterLegacy(newCreateCommand)
|
||||
commands.RegisterLegacy(newDiffCommand)
|
||||
commands.RegisterLegacy(newExportCommand)
|
||||
commands.RegisterLegacy(newKillCommand)
|
||||
commands.RegisterLegacy(newLogsCommand)
|
||||
commands.RegisterLegacy(newPauseCommand)
|
||||
commands.RegisterLegacy(newPortCommand)
|
||||
commands.RegisterLegacy(newRenameCommand)
|
||||
commands.RegisterLegacy(newRestartCommand)
|
||||
commands.RegisterLegacy(newRmCommand)
|
||||
commands.RegisterLegacy(newStartCommand)
|
||||
commands.RegisterLegacy(newStatsCommand)
|
||||
commands.RegisterLegacy(newStopCommand)
|
||||
commands.RegisterLegacy(newTopCommand)
|
||||
commands.RegisterLegacy(newUnpauseCommand)
|
||||
commands.RegisterLegacy(newUpdateCommand)
|
||||
commands.RegisterLegacy(newWaitCommand)
|
||||
// NewContainerCommand returns a cobra command for `container` subcommands
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewContainerCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newContainerCommand(dockerCLI)
|
||||
}
|
||||
|
||||
// newContainerCommand returns a cobra command for `container` subcommands
|
||||
func newContainerCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "container",
|
||||
Short: "Manage containers",
|
||||
Args: cli.NoArgs,
|
||||
RunE: command.ShowHelp(dockerCLI.Err()),
|
||||
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newAttachCommand(dockerCLI),
|
||||
|
||||
@@ -2,14 +2,13 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -18,13 +17,18 @@ type commitOptions struct {
|
||||
reference string
|
||||
|
||||
pause bool
|
||||
noPause bool
|
||||
comment string
|
||||
author string
|
||||
changes opts.ListOpts
|
||||
}
|
||||
|
||||
// newCommitCommand creates a new cobra.Command for `docker commit`
|
||||
// NewCommitCommand creates a new cobra.Command for `docker commit`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCommitCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options commitOptions
|
||||
|
||||
@@ -37,29 +41,18 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
if len(args) > 1 {
|
||||
options.reference = args[1]
|
||||
}
|
||||
if cmd.Flag("pause").Changed {
|
||||
if cmd.Flag("no-pause").Changed {
|
||||
return errors.New("conflicting options: --no-pause and --pause cannot be used together")
|
||||
}
|
||||
options.noPause = !options.pause
|
||||
}
|
||||
return runCommit(cmd.Context(), dockerCLI, &options)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container commit, docker commit",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SetInterspersed(false)
|
||||
|
||||
// TODO(thaJeztah): Deprecated: the --pause flag was deprecated in v29 and can be removed in v30.
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit (deprecated: use --no-pause instead)")
|
||||
_ = flags.MarkDeprecated("pause", "and enabled by default. Use --no-pause to disable pausing during commit.")
|
||||
|
||||
flags.BoolVar(&options.noPause, "no-pause", false, "Disable pausing container during commit")
|
||||
flags.BoolVarP(&options.pause, "pause", "p", true, "Pause container during commit")
|
||||
flags.StringVarP(&options.comment, "message", "m", "", "Commit message")
|
||||
flags.StringVarP(&options.author, "author", "a", "", `Author (e.g., "John Hannibal Smith <hannibal@a-team.com>")`)
|
||||
|
||||
@@ -70,17 +63,17 @@ func newCommitCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCommit(ctx context.Context, dockerCli command.Cli, options *commitOptions) error {
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, client.ContainerCommitOptions{
|
||||
response, err := dockerCli.Client().ContainerCommit(ctx, options.container, container.CommitOptions{
|
||||
Reference: options.reference,
|
||||
Comment: options.comment,
|
||||
Author: options.author,
|
||||
Changes: options.changes.GetSlice(),
|
||||
NoPause: options.noPause,
|
||||
Pause: options.pause,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
fmt.Fprintln(dockerCli.Out(), response.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,21 +7,25 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunCommit(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
assert.Check(t, is.Equal(options.Author, "Author Name <author@name.com>"))
|
||||
assert.Check(t, is.DeepEqual(options.Changes, []string{"EXPOSE 80"}))
|
||||
assert.Check(t, is.Equal(options.Comment, "commit message"))
|
||||
assert.Check(t, is.Equal(options.NoPause, true))
|
||||
assert.Check(t, is.Equal(options.Pause, false))
|
||||
assert.Check(t, is.Equal(ctr, "container-id"))
|
||||
|
||||
return client.ContainerCommitResult{ID: "image-id"}, nil
|
||||
return container.CommitResponse{ID: "image-id"}, nil
|
||||
},
|
||||
})
|
||||
|
||||
@@ -32,7 +36,7 @@ func TestRunCommit(t *testing.T) {
|
||||
"--author", "Author Name <author@name.com>",
|
||||
"--change", "EXPOSE 80",
|
||||
"--message", "commit message",
|
||||
"--no-pause",
|
||||
"--pause=false",
|
||||
"container-id",
|
||||
},
|
||||
)
|
||||
@@ -47,8 +51,12 @@ func TestRunCommitClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCommitFunc: func(ctx context.Context, ctr string, options client.ContainerCommitOptions) (client.ContainerCommitResult, error) {
|
||||
return client.ContainerCommitResult{}, clientError
|
||||
containerCommitFunc: func(
|
||||
ctx context.Context,
|
||||
ctr string,
|
||||
options container.CommitOptions,
|
||||
) (container.CommitResponse, error) {
|
||||
return container.CommitResponse{}, clientError
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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.23
|
||||
|
||||
package container
|
||||
|
||||
@@ -8,8 +8,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/capability"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -123,15 +122,15 @@ func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
|
||||
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
|
||||
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
|
||||
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
|
||||
@@ -182,45 +181,16 @@ 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.
|
||||
func completeLogDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := res.Info.Plugins.Log
|
||||
drivers := info.Plugins.Log
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
@@ -309,12 +279,12 @@ func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.She
|
||||
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
|
||||
func completeVolumeDriver(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
res, err := dockerCLI.Client().Info(cmd.Context(), client.InfoOptions{})
|
||||
info, err := dockerCLI.Client().Info(cmd.Context())
|
||||
if err != nil {
|
||||
// fallback: the built-in drivers
|
||||
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
drivers := res.Info.Plugins.Volume
|
||||
drivers := info.Plugins.Volume
|
||||
return drivers, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/moby/sys/signal"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
@@ -27,7 +26,7 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
|
||||
|
||||
func TestCompletePid(t *testing.T) {
|
||||
tests := []struct {
|
||||
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
|
||||
containerListFunc func(container.ListOptions) ([]container.Summary, error)
|
||||
toComplete string
|
||||
expectedCompletions []string
|
||||
expectedDirective cobra.ShellCompDirective
|
||||
@@ -43,12 +42,10 @@ func TestCompletePid(t *testing.T) {
|
||||
expectedDirective: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
|
||||
return client.ContainerListResult{
|
||||
Items: []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
},
|
||||
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
|
||||
return []container.Summary{
|
||||
*builders.Container("c1"),
|
||||
*builders.Container("c2"),
|
||||
}, nil
|
||||
},
|
||||
toComplete: "container:",
|
||||
@@ -135,44 +132,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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+53
-114
@@ -3,7 +3,6 @@ package container
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -16,10 +15,11 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/moby/go-archive"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -121,7 +121,13 @@ func copyProgress(ctx context.Context, dst io.Writer, header string, total *int6
|
||||
return restore, done
|
||||
}
|
||||
|
||||
// newCopyCommand creates a new `docker cp` command
|
||||
// NewCopyCommand creates a new `docker cp` command
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCopyCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCopyCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts copyOptions
|
||||
|
||||
@@ -154,11 +160,10 @@ container source to stdout.`,
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container cp, docker cp",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
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 +173,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,16 +232,14 @@ 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{
|
||||
Path: srcPath,
|
||||
})
|
||||
srcStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, srcPath)
|
||||
|
||||
// If the destination is a symbolic link, we should follow it.
|
||||
if err == nil && src.Stat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := src.Stat.LinkTarget
|
||||
if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := srcStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
srcParent, _ := archive.SplitPathDirEntry(srcPath)
|
||||
@@ -295,14 +254,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
cpRes, err := apiClient.CopyFromContainer(ctx, copyConfig.container, client.CopyFromContainerOptions{
|
||||
SourcePath: srcPath,
|
||||
})
|
||||
content, stat, err := apiClient.CopyFromContainer(ctx, copyConfig.container, srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content := cpRes.Content
|
||||
defer func() { _ = content.Close() }()
|
||||
defer content.Close()
|
||||
|
||||
if dstPath == "-" {
|
||||
_, err = io.Copy(dockerCLI.Out(), content)
|
||||
@@ -312,7 +268,7 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
srcInfo := archive.CopyInfo{
|
||||
Path: srcPath,
|
||||
Exists: true,
|
||||
IsDir: cpRes.Stat.Mode.IsDir(),
|
||||
IsDir: stat.Mode.IsDir(),
|
||||
RebaseName: rebaseName,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -352,66 +304,63 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
|
||||
// about both the source and destination. The API is a simple tar
|
||||
// archive/extract API but we can use the stat info header about the
|
||||
// destination to be more informed about exactly what the destination is.
|
||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) error {
|
||||
func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpConfig) (err error) {
|
||||
srcPath := copyConfig.sourcePath
|
||||
dstPath := copyConfig.destPath
|
||||
|
||||
if srcPath != "-" {
|
||||
// Get an absolute source path.
|
||||
p, err := resolveLocalPath(srcPath)
|
||||
srcPath, err = resolveLocalPath(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcPath = p
|
||||
}
|
||||
|
||||
apiClient := dockerCLI.Client()
|
||||
// Prepare destination copy info by stat-ing the container path.
|
||||
dstInfo := archive.CopyInfo{Path: dstPath}
|
||||
if dst, err := apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: dstPath}); err == nil {
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if dst.Stat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dst.Stat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
dstStat, err := apiClient.ContainerStatPath(ctx, copyConfig.container, dstPath)
|
||||
|
||||
dstInfo.Path = linkTarget
|
||||
dst, err = apiClient.ContainerStatPath(ctx, copyConfig.container, client.ContainerStatPathOptions{Path: linkTarget})
|
||||
}
|
||||
// Validate the destination path
|
||||
if err == nil {
|
||||
if err := command.ValidateOutputPathFileMode(dst.Stat.Mode); err != nil {
|
||||
return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, copyConfig.container, dstPath, err)
|
||||
}
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dst.Stat.Mode.IsDir()
|
||||
// If the destination is a symbolic link, we should evaluate it.
|
||||
if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
|
||||
linkTarget := dstStat.LinkTarget
|
||||
if !isAbs(linkTarget) {
|
||||
// Join with the parent directory.
|
||||
dstParent, _ := archive.SplitPathDirEntry(dstPath)
|
||||
linkTarget = filepath.Join(dstParent, linkTarget)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
_ = err // Intentionally ignore stat errors (see above)
|
||||
dstInfo.Path = linkTarget
|
||||
dstStat, err = apiClient.ContainerStatPath(ctx, copyConfig.container, linkTarget)
|
||||
// FIXME(thaJeztah): unhandled error (should this return?)
|
||||
}
|
||||
|
||||
// Validate the destination path
|
||||
if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
|
||||
return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath)
|
||||
}
|
||||
|
||||
// Ignore any error and assume that the parent directory of the destination
|
||||
// path exists, in which case the copy may still succeed. If there is any
|
||||
// type of conflict (e.g., non-directory overwriting an existing directory
|
||||
// or vice versa) the extraction will fail. If the destination simply did
|
||||
// not exist, but the parent directory does, the extraction will still
|
||||
// succeed.
|
||||
if err == nil {
|
||||
dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
|
||||
}
|
||||
|
||||
var (
|
||||
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)
|
||||
return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath)
|
||||
}
|
||||
} else {
|
||||
// Prepare source copy info.
|
||||
@@ -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
|
||||
@@ -456,31 +403,23 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
|
||||
}
|
||||
}
|
||||
|
||||
options := client.CopyToContainerOptions{
|
||||
DestinationPath: resolvedDstPath,
|
||||
Content: content,
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
options := container.CopyToContainerOptions{
|
||||
CopyUIDGID: copyConfig.copyUIDGID,
|
||||
}
|
||||
|
||||
if copyConfig.quiet {
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
return err
|
||||
return apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||
restore, done := copyProgress(ctx, dockerCLI.Err(), copyToContainerHeader, &copiedSize)
|
||||
// TODO(thaJeztah): error-handling looks odd here; should it be handled differently?
|
||||
_, err := apiClient.CopyToContainer(ctx, copyConfig.container, options)
|
||||
res := apiClient.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options)
|
||||
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
|
||||
return res
|
||||
}
|
||||
|
||||
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
|
||||
|
||||
@@ -9,10 +9,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"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"
|
||||
"gotest.tools/v3/fs"
|
||||
@@ -53,11 +52,9 @@ func TestRunCopyFromContainerToStdout(t *testing.T) {
|
||||
tarContent := "the tar content"
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
return client.CopyFromContainerResult{
|
||||
Content: io.NopCloser(strings.NewReader(tarContent)),
|
||||
}, nil
|
||||
return io.NopCloser(strings.NewReader(tarContent)), container.PathStat{}, nil
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@@ -76,12 +73,10 @@ func TestRunCopyFromContainerToFilesystem(t *testing.T) {
|
||||
destDir := fs.NewDir(t, "cp-test")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
return readCloser, container.PathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@@ -104,12 +99,10 @@ func TestRunCopyFromContainerToFilesystemMissingDestinationDirectory(t *testing.
|
||||
defer destDir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
|
||||
containerCopyFromFunc: func(ctr, srcPath string) (io.ReadCloser, container.PathStat, error) {
|
||||
assert.Check(t, is.Equal("container", ctr))
|
||||
readCloser, err := archive.TarWithOptions(destDir.Path(), &archive.TarOptions{})
|
||||
return client.CopyFromContainerResult{
|
||||
Content: readCloser,
|
||||
}, err
|
||||
return readCloser, container.PathStat{}, err
|
||||
},
|
||||
})
|
||||
err := runCopy(context.TODO(), cli, copyOptions{
|
||||
@@ -212,237 +205,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"))
|
||||
}
|
||||
|
||||
+133
-109
@@ -4,9 +4,9 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
@@ -17,14 +17,20 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/mount"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
imagetypes "github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -39,12 +45,19 @@ const (
|
||||
type createOptions struct {
|
||||
name string
|
||||
platform string
|
||||
untrusted bool
|
||||
pull string // always, missing, never
|
||||
quiet bool
|
||||
useAPISocket bool
|
||||
}
|
||||
|
||||
// newCreateCommand creates a new cobra.Command for `docker create`
|
||||
// NewCreateCommand creates a new cobra.Command for `docker create`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newCreateCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var options createOptions
|
||||
var copts *containerOptions
|
||||
@@ -63,47 +76,50 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container create, docker create",
|
||||
},
|
||||
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ImageNames(dockerCLI, -1),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
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.
|
||||
flags.SetAnnotation("use-api-socket", "experimentalCLI", nil) // Marks flag as experimental for now.
|
||||
|
||||
// Add an explicit help that doesn't have a `-h` to prevent the conflict
|
||||
// with hostname
|
||||
flags.Bool("help", false, "Print usage")
|
||||
|
||||
// TODO(thaJeztah): consider adding platform as "image create option" on containerOptions
|
||||
flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable")
|
||||
_ = flags.SetAnnotation("platform", "version", []string{"1.32"})
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms())
|
||||
addPlatformFlag(flags, &options.platform)
|
||||
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
|
||||
|
||||
// TODO(thaJeztah): DEPRECATED: remove in v29.1 or v30
|
||||
flags.Bool("disable-content-trust", true, "Skip image verification (deprecated)")
|
||||
_ = flags.MarkDeprecated("disable-content-trust", "support for docker content trust was removed")
|
||||
flags.BoolVar(&options.untrusted, "disable-content-trust", !dockerCLI.ContentTrustEnabled(), "Skip image verification")
|
||||
copts = addFlags(flags)
|
||||
|
||||
addCompletions(cmd, dockerCLI)
|
||||
|
||||
flags.VisitAll(func(flag *pflag.Flag) {
|
||||
// Set a default completion function if none was set. We don't look
|
||||
// up if it does already have one set, because Cobra does this for
|
||||
// us, and returns an error (which we ignore for this reason).
|
||||
_ = cmd.RegisterFlagCompletionFunc(flag.Name, cobra.NoFileCompletions)
|
||||
})
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
func runCreate(ctx context.Context, dockerCli command.Cli, flags *pflag.FlagSet, options *createOptions, copts *containerOptions) error {
|
||||
if err := validatePullOpt(options.pull); err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
proxyConfig := dockerCLI.ConfigFile().ParseProxyConfig(dockerCLI.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
newEnv := make([]string, 0, len(proxyConfig))
|
||||
proxyConfig := dockerCli.ConfigFile().ParseProxyConfig(dockerCli.Client().DaemonHost(), opts.ConvertKVStringsToMapWithNil(copts.env.GetSlice()))
|
||||
newEnv := []string{}
|
||||
for k, v := range proxyConfig {
|
||||
if v == nil {
|
||||
newEnv = append(newEnv, k)
|
||||
@@ -112,53 +128,42 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, flags *pflag.FlagSet,
|
||||
}
|
||||
}
|
||||
copts.env = *opts.NewListOptsRef(&newEnv, nil)
|
||||
serverInfo, err := dockerCLI.Client().Ping(ctx, client.PingOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerCfg, err := parse(flags, copts, serverInfo.OSType)
|
||||
containerCfg, err := parse(flags, copts, dockerCli.ServerInfo().OSType)
|
||||
if err != nil {
|
||||
return cli.StatusError{
|
||||
Status: withHelp(err, "create").Error(),
|
||||
StatusCode: 125,
|
||||
}
|
||||
}
|
||||
id, err := createContainer(ctx, dockerCLI, containerCfg, options)
|
||||
id, err := createContainer(ctx, dockerCli, containerCfg, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = fmt.Fprintln(dockerCLI.Out(), id)
|
||||
_, _ = fmt.Fprintln(dockerCli.Out(), id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, dockerCLI command.Cli, img string, options *createOptions) error {
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCLI.ConfigFile(), img)
|
||||
// FIXME(thaJeztah): this is the only code-path that uses APIClient.ImageCreate. Rewrite this to use the regular "pull" code (or vice-versa).
|
||||
func pullImage(ctx context.Context, dockerCli command.Cli, img string, options *createOptions) error {
|
||||
encodedAuth, err := command.RetrieveAuthTokenFromImage(dockerCli.ConfigFile(), img)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ociPlatforms []ocispec.Platform
|
||||
if options.platform != "" {
|
||||
// Already validated.
|
||||
ociPlatforms = append(ociPlatforms, platforms.MustParse(options.platform))
|
||||
}
|
||||
resp, err := dockerCLI.Client().ImagePull(ctx, img, client.ImagePullOptions{
|
||||
responseBody, err := dockerCli.Client().ImageCreate(ctx, img, imagetypes.CreateOptions{
|
||||
RegistryAuth: encodedAuth,
|
||||
Platforms: ociPlatforms,
|
||||
Platform: options.platform,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Close()
|
||||
}()
|
||||
defer responseBody.Close()
|
||||
|
||||
out := dockerCLI.Err()
|
||||
out := dockerCli.Err()
|
||||
if options.quiet {
|
||||
out = streams.NewOut(io.Discard)
|
||||
}
|
||||
return jsonstream.Display(ctx, resp, out)
|
||||
return jsonstream.Display(ctx, responseBody, out)
|
||||
}
|
||||
|
||||
type cidFile struct {
|
||||
@@ -171,13 +176,13 @@ func (cid *cidFile) Close() error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
_ = cid.file.Close()
|
||||
cid.file.Close()
|
||||
|
||||
if cid.written {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(cid.path); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to remove the CID file: %w", err)
|
||||
if err := os.Remove(cid.path); err != nil {
|
||||
return errors.Wrapf(err, "failed to remove the CID file '%s'", cid.path)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -187,8 +192,8 @@ func (cid *cidFile) Write(id string) error {
|
||||
if cid.file == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := cid.file.WriteString(id); err != nil {
|
||||
return fmt.Errorf("failed to write the container ID (%s) to file: %w", id, err)
|
||||
if _, err := cid.file.Write([]byte(id)); err != nil {
|
||||
return errors.Wrap(err, "failed to write the container ID to the file")
|
||||
}
|
||||
cid.written = true
|
||||
return nil
|
||||
@@ -199,39 +204,33 @@ func newCIDFile(cidPath string) (*cidFile, error) {
|
||||
return &cidFile{}, nil
|
||||
}
|
||||
if _, err := os.Stat(cidPath); err == nil {
|
||||
return nil, errors.New("container ID file found, make sure the other container isn't running or delete " + cidPath)
|
||||
return nil, errors.Errorf("container ID file found, make sure the other container isn't running or delete %s", cidPath)
|
||||
}
|
||||
|
||||
f, err := os.Create(cidPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the container ID file: %w", err)
|
||||
return nil, errors.Wrap(err, "failed to create the container ID file")
|
||||
}
|
||||
|
||||
return &cidFile{path: cidPath, file: f}, nil
|
||||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, _ error) {
|
||||
func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *containerConfig, options *createOptions) (containerID string, err error) {
|
||||
config := containerCfg.Config
|
||||
hostConfig := containerCfg.HostConfig
|
||||
networkingConfig := containerCfg.NetworkingConfig
|
||||
|
||||
var namedRef reference.Named
|
||||
|
||||
// TODO(thaJeztah): add a platform option-type / flag-type.
|
||||
if options.platform != "" {
|
||||
if _, err := platforms.Parse(options.platform); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
var (
|
||||
trustedRef reference.Canonical
|
||||
namedRef reference.Named
|
||||
)
|
||||
|
||||
containerIDFile, err := newCIDFile(hostConfig.ContainerIDFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = containerIDFile.Close()
|
||||
}()
|
||||
defer containerIDFile.Close()
|
||||
|
||||
ref, err := reference.ParseAnyReference(config.Image)
|
||||
if err != nil {
|
||||
@@ -239,6 +238,15 @@ func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *c
|
||||
}
|
||||
if named, ok := ref.(reference.Named); ok {
|
||||
namedRef = reference.TagNameOnly(named)
|
||||
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && !options.untrusted {
|
||||
var err error
|
||||
trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
config.Image = reference.FamiliarString(trustedRef)
|
||||
}
|
||||
}
|
||||
|
||||
const dockerConfigPathInContainer = "/run/secrets/docker/config.json"
|
||||
@@ -246,11 +254,10 @@ func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *c
|
||||
|
||||
if options.useAPISocket {
|
||||
// We'll create two new mounts to handle this flag:
|
||||
//
|
||||
// 1. Mount the actual docker socket.
|
||||
// 2. A synthesized ~/.docker/config.json with resolved tokens.
|
||||
// 2. A synthezised ~/.docker/config.json with resolved tokens.
|
||||
|
||||
if dockerCLI.ServerInfo().OSType == "windows" {
|
||||
if dockerCli.ServerInfo().OSType == "windows" {
|
||||
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
|
||||
}
|
||||
|
||||
@@ -285,18 +292,18 @@ func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *c
|
||||
})
|
||||
*/
|
||||
|
||||
var envVarPresent bool
|
||||
for _, envVar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envVar, "DOCKER_CONFIG=") {
|
||||
envVarPresent = true
|
||||
var envvarPresent bool
|
||||
for _, envvar := range containerCfg.Config.Env {
|
||||
if strings.HasPrefix(envvar, "DOCKER_CONFIG=") {
|
||||
envvarPresent = true
|
||||
}
|
||||
}
|
||||
|
||||
// If the DOCKER_CONFIG env var is already present, we assume the client knows
|
||||
// what they're doing and don't inject the creds.
|
||||
if !envVarPresent {
|
||||
if !envvarPresent {
|
||||
// Resolve this here for later, ensuring we error our before we create the container.
|
||||
creds, err := readCredentials(dockerCLI)
|
||||
creds, err := readCredentials(dockerCli)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolving credentials failed: %w", err)
|
||||
}
|
||||
@@ -310,51 +317,51 @@ func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *c
|
||||
}
|
||||
|
||||
var platform *ocispec.Platform
|
||||
if options.platform != "" {
|
||||
// Engine API version 1.41 first introduced the option to specify platform on
|
||||
// create. It will produce an error if you try to set a platform on older API
|
||||
// versions, so check the API version here to maintain backwards
|
||||
// compatibility for CLI users.
|
||||
if options.platform != "" && versions.GreaterThanOrEqualTo(dockerCli.Client().ClientVersion(), "1.41") {
|
||||
p, err := platforms.Parse(options.platform)
|
||||
if err != nil {
|
||||
return "", invalidParameter(fmt.Errorf("error parsing specified platform: %w", err))
|
||||
return "", errors.Wrap(invalidParameter(err), "error parsing specified platform")
|
||||
}
|
||||
platform = &p
|
||||
}
|
||||
|
||||
pullAndTagImage := func() error {
|
||||
if err := pullImage(ctx, dockerCli, config.Image, options); err != nil {
|
||||
return err
|
||||
}
|
||||
if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil {
|
||||
return trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), trustedRef, taggedRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if options.pull == PullImageAlways {
|
||||
if err := pullImage(ctx, dockerCLI, config.Image, options); err != nil {
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCLI.Out().GetTtySize()
|
||||
hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize()
|
||||
|
||||
response, err := dockerCLI.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
})
|
||||
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
if err != nil {
|
||||
// Pull image if it does not exist locally and we have the PullImageMissing option. Default behavior.
|
||||
if errdefs.IsNotFound(err) && namedRef != nil && options.pull == PullImageMissing {
|
||||
if !options.quiet {
|
||||
// we don't want to write to stdout anything apart from container.ID
|
||||
_, _ = fmt.Fprintf(dockerCLI.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
_, _ = fmt.Fprintf(dockerCli.Err(), "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef))
|
||||
}
|
||||
|
||||
if err := pullImage(ctx, dockerCLI, config.Image, options); err != nil {
|
||||
if err := pullAndTagImage(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var retryErr error
|
||||
response, retryErr = dockerCLI.Client().ContainerCreate(ctx, client.ContainerCreateOptions{
|
||||
Name: options.name,
|
||||
// Image: config.Image, // TODO(thaJeztah): pass image-ref separate
|
||||
Platform: platform,
|
||||
Config: config,
|
||||
HostConfig: hostConfig,
|
||||
NetworkingConfig: networkingConfig,
|
||||
})
|
||||
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, options.name)
|
||||
if retryErr != nil {
|
||||
return "", retryErr
|
||||
}
|
||||
@@ -363,20 +370,41 @@ func createContainer(ctx context.Context, dockerCLI command.Cli, containerCfg *c
|
||||
}
|
||||
}
|
||||
|
||||
if options.useAPISocket && len(apiSocketCreds) > 0 {
|
||||
// Create a new config file with just the auth.
|
||||
if err := copyDockerConfigIntoContainer(ctx, dockerCLI.Client(), response.ID, dockerConfigPathInContainer, &configfile.ConfigFile{
|
||||
AuthConfigs: apiSocketCreds,
|
||||
}); err != nil {
|
||||
response.Warnings = append(response.Warnings, fmt.Sprintf("injecting docker config.json into container failed: %v", err))
|
||||
}
|
||||
}
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING:", w)
|
||||
if warn := localhostDNSWarning(*hostConfig); warn != "" {
|
||||
response.Warnings = append(response.Warnings, warn)
|
||||
}
|
||||
|
||||
err = containerIDFile.Write(response.ID)
|
||||
return response.ID, err
|
||||
containerID = response.ID
|
||||
for _, w := range response.Warnings {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "WARNING:", w)
|
||||
}
|
||||
err = containerIDFile.Write(containerID)
|
||||
|
||||
if options.useAPISocket && len(apiSocketCreds) > 0 {
|
||||
// Create a new config file with just the auth.
|
||||
newConfig := &configfile.ConfigFile{
|
||||
AuthConfigs: apiSocketCreds,
|
||||
}
|
||||
|
||||
if err := copyDockerConfigIntoContainer(ctx, dockerCli.Client(), containerID, dockerConfigPathInContainer, newConfig); err != nil {
|
||||
return "", fmt.Errorf("injecting docker config.json into container failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return containerID, err
|
||||
}
|
||||
|
||||
// check the DNS settings passed via --dns against localhost regexp to warn if
|
||||
// they are trying to set a DNS to a localhost address.
|
||||
//
|
||||
// TODO(thaJeztah): move this to the daemon, which can make a better call if it will work or not (depending on networking mode).
|
||||
func localhostDNSWarning(hostConfig container.HostConfig) string {
|
||||
for _, dnsIP := range hostConfig.DNS {
|
||||
if addr, err := netip.ParseAddr(dnsIP); err == nil && addr.IsLoopback() {
|
||||
return fmt.Sprintf("Localhost DNS (%s) may fail in containers.", addr)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validatePullOpt(val string) error {
|
||||
@@ -400,7 +428,7 @@ func validatePullOpt(val string) error {
|
||||
//
|
||||
// The path should be an absolute path in the container, commonly
|
||||
// /root/.docker/config.json.
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
func copyDockerConfigIntoContainer(ctx context.Context, dockerAPI client.APIClient, containerID string, configPath string, config *configfile.ConfigFile) error {
|
||||
var configBuf bytes.Buffer
|
||||
if err := config.SaveToWriter(&configBuf); err != nil {
|
||||
return fmt.Errorf("saving creds: %w", err)
|
||||
@@ -409,14 +437,13 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
|
||||
// We don't need to get super fancy with the tar creation.
|
||||
var tarBuf bytes.Buffer
|
||||
tarWriter := tar.NewWriter(&tarBuf)
|
||||
_ = tarWriter.WriteHeader(&tar.Header{
|
||||
tarWriter.WriteHeader(&tar.Header{
|
||||
Name: configPath,
|
||||
Size: int64(configBuf.Len()),
|
||||
Mode: 0o600,
|
||||
})
|
||||
|
||||
if _, err := io.Copy(tarWriter, &configBuf); err != nil {
|
||||
_ = tarWriter.Close()
|
||||
return fmt.Errorf("writing config to tar file for config copy: %w", err)
|
||||
}
|
||||
|
||||
@@ -424,11 +451,8 @@ func copyDockerConfigIntoContainer(ctx context.Context, apiClient client.APIClie
|
||||
return fmt.Errorf("closing tar for config copy failed: %w", err)
|
||||
}
|
||||
|
||||
_, err := apiClient.CopyToContainer(ctx, containerID, client.CopyToContainerOptions{
|
||||
DestinationPath: "/",
|
||||
Content: &tarBuf,
|
||||
})
|
||||
if err != nil {
|
||||
if err := dockerAPI.CopyToContainer(ctx, containerID, "/",
|
||||
&tarBuf, container.CopyToContainerOptions{}); err != nil {
|
||||
return fmt.Errorf("copying config.json into container failed: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -14,10 +13,13 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/notary"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/api/types/system"
|
||||
"github.com/moby/moby/client"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/spf13/pflag"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
@@ -43,31 +45,17 @@ func TestNewCIDFileWhenFileAlreadyExists(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithNoWrite(t *testing.T) {
|
||||
// Closing should remove the file if it was not written to.
|
||||
t.Run("closing should remove file", func(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "cidfile-1")
|
||||
file, err := newCIDFile(filename)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, filename))
|
||||
tempdir := fs.NewDir(t, "test-cid-file")
|
||||
defer tempdir.Remove()
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(filename)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
})
|
||||
path := tempdir.Join("cidfile")
|
||||
file, err := newCIDFile(path)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, path))
|
||||
|
||||
// Closing (and removing) the file should not produce an error if the file no longer exists.
|
||||
t.Run("close should remove file", func(t *testing.T) {
|
||||
filename := filepath.Join(t.TempDir(), "cidfile-2")
|
||||
file, err := newCIDFile(filename)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(file.path, filename))
|
||||
|
||||
assert.NilError(t, os.Remove(filename))
|
||||
_, err = os.Stat(filename)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
|
||||
assert.NilError(t, file.Close())
|
||||
})
|
||||
assert.NilError(t, file.Close())
|
||||
_, err = os.Stat(path)
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
}
|
||||
|
||||
func TestCIDFileCloseWithWrite(t *testing.T) {
|
||||
@@ -129,30 +117,35 @@ func TestCreateContainerImagePullPolicy(t *testing.T) {
|
||||
pullCounter := 0
|
||||
|
||||
apiClient := &fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
createContainerFunc: func(
|
||||
config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
defer func() { tc.ResponseCounter++ }()
|
||||
switch tc.ResponseCounter {
|
||||
case 0:
|
||||
return client.ContainerCreateResult{}, fakeNotFound{}
|
||||
return container.CreateResponse{}, fakeNotFound{}
|
||||
default:
|
||||
return client.ContainerCreateResult{ID: containerID}, nil
|
||||
return container.CreateResponse{ID: containerID}, nil
|
||||
}
|
||||
},
|
||||
imagePullFunc: func(ctx context.Context, parentReference string, options client.ImagePullOptions) (client.ImagePullResponse, error) {
|
||||
imageCreateFunc: func(ctx context.Context, parentReference string, options image.CreateOptions) (io.ReadCloser, error) {
|
||||
defer func() { pullCounter++ }()
|
||||
return fakeStreamResult{ReadCloser: io.NopCloser(strings.NewReader(""))}, nil
|
||||
return io.NopCloser(strings.NewReader("")), nil
|
||||
},
|
||||
infoFunc: func() (client.SystemInfoResult, error) {
|
||||
return client.SystemInfoResult{
|
||||
Info: system.Info{IndexServerAddress: "https://indexserver.example.com"},
|
||||
}, nil
|
||||
infoFunc: func() (system.Info, error) {
|
||||
return system.Info{IndexServerAddress: "https://indexserver.example.com"}, nil
|
||||
},
|
||||
}
|
||||
fakeCLI := test.NewFakeCli(apiClient)
|
||||
id, err := createContainer(context.Background(), fakeCLI, config, &createOptions{
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
pull: tc.PullPolicy,
|
||||
name: "name",
|
||||
platform: runtime.GOOS,
|
||||
untrusted: true,
|
||||
pull: tc.PullPolicy,
|
||||
})
|
||||
|
||||
if tc.ExpectedErrMsg != "" {
|
||||
@@ -228,6 +221,55 @@ func TestCreateContainerValidateFlags(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreateCommandWithContentTrustErrors(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
notaryFunc test.NotaryClientFuncType
|
||||
}{
|
||||
{
|
||||
name: "offline-notary-server",
|
||||
notaryFunc: notary.GetOfflineNotaryRepository,
|
||||
expectedError: "client is offline",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "uninitialized-notary-server",
|
||||
notaryFunc: notary.GetUninitializedNotaryRepository,
|
||||
expectedError: "remote trust data does not exist",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
{
|
||||
name: "empty-notary-server",
|
||||
notaryFunc: notary.GetEmptyTargetsNotaryRepository,
|
||||
expectedError: "No valid trust data for tag",
|
||||
args: []string{"image:tag"},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{}, errors.New("shouldn't try to pull image")
|
||||
},
|
||||
}, test.EnableContentTrust)
|
||||
fakeCLI.SetNotaryClient(tc.notaryFunc)
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
cmd.SetOut(io.Discard)
|
||||
cmd.SetErr(io.Discard)
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -249,12 +291,27 @@ func TestNewCreateCommandWithWarnings(t *testing.T) {
|
||||
args: []string{"image:tag"},
|
||||
warnings: []string{"warning from daemon", "another warning from daemon"},
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns",
|
||||
args: []string{"--dns=127.0.0.11", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
{
|
||||
name: "container-create-localhost-dns-ipv6",
|
||||
args: []string{"--dns=::1", "image:tag"},
|
||||
warning: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
return client.ContainerCreateResult{Warnings: tc.warnings}, nil
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
return container.CreateResponse{Warnings: tc.warnings}, nil
|
||||
},
|
||||
})
|
||||
cmd := newCreateCommand(fakeCLI)
|
||||
@@ -287,10 +344,15 @@ func TestCreateContainerWithProxyConfig(t *testing.T) {
|
||||
sort.Strings(expected)
|
||||
|
||||
fakeCLI := test.NewFakeCli(&fakeClient{
|
||||
createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) {
|
||||
sort.Strings(options.Config.Env)
|
||||
assert.DeepEqual(t, options.Config.Env, expected)
|
||||
return client.ContainerCreateResult{}, nil
|
||||
createContainerFunc: func(config *container.Config,
|
||||
hostConfig *container.HostConfig,
|
||||
networkingConfig *network.NetworkingConfig,
|
||||
platform *ocispec.Platform,
|
||||
containerName string,
|
||||
) (container.CreateResponse, error) {
|
||||
sort.Strings(config.Env)
|
||||
assert.DeepEqual(t, config.Env, expected)
|
||||
return container.CreateResponse{}, nil
|
||||
},
|
||||
})
|
||||
fakeCLI.SetConfigFile(&configfile.ConfigFile{
|
||||
|
||||
@@ -7,11 +7,16 @@ import (
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// newDiffCommand creates a new cobra.Command for `docker diff`
|
||||
// NewDiffCommand creates a new cobra.Command for `docker diff`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newDiffCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "diff CONTAINER",
|
||||
@@ -23,13 +28,12 @@ func newDiffCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container diff, docker diff",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, false),
|
||||
}
|
||||
}
|
||||
|
||||
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
|
||||
res, err := dockerCLI.Client().ContainerDiff(ctx, containerID, client.ContainerDiffOptions{})
|
||||
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -37,5 +41,5 @@ func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) err
|
||||
Output: dockerCLI.Out(),
|
||||
Format: newDiffFormat("{{.Type}} {{.Path}}"),
|
||||
}
|
||||
return diffFormatWrite(diffCtx, res)
|
||||
return diffFormatWrite(diffCtx, changes)
|
||||
}
|
||||
|
||||
@@ -8,29 +8,29 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestRunDiff(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
|
||||
return client.ContainerDiffResult{
|
||||
Changes: []container.FilesystemChange{
|
||||
{
|
||||
Kind: container.ChangeModify,
|
||||
Path: "/path/to/file0",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeAdd,
|
||||
Path: "/path/to/file1",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeDelete,
|
||||
Path: "/path/to/file2",
|
||||
},
|
||||
containerDiffFunc: func(
|
||||
ctx context.Context,
|
||||
containerID string,
|
||||
) ([]container.FilesystemChange, error) {
|
||||
return []container.FilesystemChange{
|
||||
{
|
||||
Kind: container.ChangeModify,
|
||||
Path: "/path/to/file0",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeAdd,
|
||||
Path: "/path/to/file1",
|
||||
},
|
||||
{
|
||||
Kind: container.ChangeDelete,
|
||||
Path: "/path/to/file2",
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
@@ -60,8 +60,11 @@ func TestRunDiffClientError(t *testing.T) {
|
||||
clientError := errors.New("client error")
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerDiffFunc: func(ctx context.Context, containerID string) (client.ContainerDiffResult, error) {
|
||||
return client.ContainerDiffResult{}, clientError
|
||||
containerDiffFunc: func(
|
||||
ctx context.Context,
|
||||
containerID string,
|
||||
) ([]container.FilesystemChange, error) {
|
||||
return nil, clientError
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
@@ -11,8 +10,9 @@ import (
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -39,7 +39,13 @@ func NewExecOptions() ExecOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// newExecCommand creates a new cobra.Command for "docker exec".
|
||||
// NewExecCommand creates a new cobra.Command for `docker exec`
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newExecCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
options := NewExecOptions()
|
||||
|
||||
@@ -59,7 +65,6 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
"category-top": "2",
|
||||
"aliases": "docker container exec, docker exec",
|
||||
},
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -72,14 +77,14 @@ func newExecCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
flags.StringVarP(&options.User, "user", "u", "", `Username or UID (format: "<name|uid>[:<group|gid>]")`)
|
||||
flags.BoolVar(&options.Privileged, "privileged", false, "Give extended privileges to the command")
|
||||
flags.VarP(&options.Env, "env", "e", "Set environment variables")
|
||||
_ = flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.SetAnnotation("env", "version", []string{"1.25"})
|
||||
flags.Var(&options.EnvFile, "env-file", "Read in a file of environment variables")
|
||||
_ = flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
flags.SetAnnotation("env-file", "version", []string{"1.25"})
|
||||
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
|
||||
_ = flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
flags.SetAnnotation("workdir", "version", []string{"1.35"})
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames())
|
||||
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
|
||||
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@@ -97,18 +102,18 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
// otherwise if we error out we will leak execIDs on the server (and
|
||||
// there's no easy way to clean those up). But also in order to make "not
|
||||
// exist" errors take precedence we do a dummy inspect first.
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName, client.ContainerInspectOptions{}); err != nil {
|
||||
if _, err := apiClient.ContainerInspect(ctx, containerIDorName); err != nil {
|
||||
return err
|
||||
}
|
||||
if !options.Detach {
|
||||
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.TTY); err != nil {
|
||||
if err := dockerCLI.In().CheckTty(execOptions.AttachStdin, execOptions.Tty); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fillConsoleSize(execOptions, dockerCLI)
|
||||
|
||||
response, err := apiClient.ExecCreate(ctx, containerIDorName, *execOptions)
|
||||
response, err := apiClient.ContainerExecCreate(ctx, containerIDorName, *execOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -119,24 +124,23 @@ func RunExec(ctx context.Context, dockerCLI command.Cli, containerIDorName strin
|
||||
}
|
||||
|
||||
if options.Detach {
|
||||
_, err := apiClient.ExecStart(ctx, execID, client.ExecStartOptions{
|
||||
return apiClient.ContainerExecStart(ctx, execID, container.ExecStartOptions{
|
||||
Detach: options.Detach,
|
||||
TTY: execOptions.TTY,
|
||||
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
return err
|
||||
}
|
||||
return interactiveExec(ctx, dockerCLI, execOptions, execID)
|
||||
}
|
||||
|
||||
func fillConsoleSize(execOptions *client.ExecCreateOptions, dockerCli command.Cli) {
|
||||
if execOptions.TTY {
|
||||
func fillConsoleSize(execOptions *container.ExecOptions, dockerCli command.Cli) {
|
||||
if execOptions.Tty {
|
||||
height, width := dockerCli.Out().GetTtySize()
|
||||
execOptions.ConsoleSize = client.ConsoleSize{Height: height, Width: width}
|
||||
execOptions.ConsoleSize = &[2]uint{height, width}
|
||||
}
|
||||
}
|
||||
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *client.ExecCreateOptions, execID string) error {
|
||||
func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *container.ExecOptions, execID string) error {
|
||||
// Interactive exec requested.
|
||||
var (
|
||||
out, stderr io.Writer
|
||||
@@ -150,7 +154,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
out = dockerCli.Out()
|
||||
}
|
||||
if execOptions.AttachStderr {
|
||||
if execOptions.TTY {
|
||||
if execOptions.Tty {
|
||||
stderr = dockerCli.Out()
|
||||
} else {
|
||||
stderr = dockerCli.Err()
|
||||
@@ -159,9 +163,9 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
fillConsoleSize(execOptions, dockerCli)
|
||||
|
||||
apiClient := dockerCli.Client()
|
||||
resp, err := apiClient.ExecAttach(ctx, execID, client.ExecAttachOptions{
|
||||
TTY: execOptions.TTY,
|
||||
ConsoleSize: client.ConsoleSize{Height: execOptions.ConsoleSize.Height, Width: execOptions.ConsoleSize.Width},
|
||||
resp, err := apiClient.ContainerExecAttach(ctx, execID, container.ExecAttachOptions{
|
||||
Tty: execOptions.Tty,
|
||||
ConsoleSize: execOptions.ConsoleSize,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -178,8 +182,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
inputStream: in,
|
||||
outputStream: out,
|
||||
errorStream: stderr,
|
||||
resp: resp.HijackedResponse,
|
||||
tty: execOptions.TTY,
|
||||
resp: resp,
|
||||
tty: execOptions.Tty,
|
||||
detachKeys: execOptions.DetachKeys,
|
||||
}
|
||||
|
||||
@@ -187,7 +191,7 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
}()
|
||||
}()
|
||||
|
||||
if execOptions.TTY && dockerCli.In().IsTerminal() {
|
||||
if execOptions.Tty && dockerCli.In().IsTerminal() {
|
||||
if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil {
|
||||
_, _ = fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err)
|
||||
}
|
||||
@@ -201,8 +205,8 @@ func interactiveExec(ctx context.Context, dockerCli command.Cli, execOptions *cl
|
||||
return getExecExitStatus(ctx, apiClient, execID)
|
||||
}
|
||||
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, execID string) error {
|
||||
resp, err := apiClient.ExecInspect(ctx, execID, client.ExecInspectOptions{})
|
||||
func getExecExitStatus(ctx context.Context, apiClient client.ContainerAPIClient, execID string) error {
|
||||
resp, err := apiClient.ContainerExecInspect(ctx, execID)
|
||||
if err != nil {
|
||||
// If we can't connect, then the daemon probably died.
|
||||
if !client.IsErrConnectionFailed(err) {
|
||||
@@ -219,11 +223,11 @@ func getExecExitStatus(ctx context.Context, apiClient client.ExecAPIClient, exec
|
||||
|
||||
// parseExec parses the specified args for the specified command and generates
|
||||
// an ExecConfig from it.
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client.ExecCreateOptions, error) {
|
||||
execOptions := &client.ExecCreateOptions{
|
||||
func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*container.ExecOptions, error) {
|
||||
execOptions := &container.ExecOptions{
|
||||
User: execOpts.User,
|
||||
Privileged: execOpts.Privileged,
|
||||
TTY: execOpts.TTY,
|
||||
Tty: execOpts.TTY,
|
||||
Cmd: execOpts.Command,
|
||||
WorkingDir: execOpts.Workdir,
|
||||
}
|
||||
@@ -248,8 +252,5 @@ func parseExec(execOpts ExecOptions, configFile *configfile.ConfigFile) (*client
|
||||
} else {
|
||||
execOptions.DetachKeys = configFile.DetachKeys
|
||||
}
|
||||
if err := validateDetachKeys(execOpts.DetachKeys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return execOptions, nil
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
@@ -38,10 +38,10 @@ TWO=2
|
||||
testcases := []struct {
|
||||
options ExecOptions
|
||||
configFile configfile.ConfigFile
|
||||
expected client.ExecCreateOptions
|
||||
expected container.ExecOptions
|
||||
}{
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@@ -49,7 +49,7 @@ TWO=2
|
||||
options: withDefaultOpts(ExecOptions{}),
|
||||
},
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command1", "command2"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@@ -64,18 +64,18 @@ TWO=2
|
||||
TTY: true,
|
||||
User: "uid",
|
||||
}),
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
User: "uid",
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
TTY: true,
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
@@ -85,32 +85,32 @@ TWO=2
|
||||
Interactive: true,
|
||||
Detach: true,
|
||||
}),
|
||||
expected: client.ExecCreateOptions{
|
||||
TTY: true,
|
||||
expected: container.ExecOptions{
|
||||
Tty: true,
|
||||
Cmd: []string{"command"},
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{Detach: true}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
|
||||
expected: client.ExecCreateOptions{
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ctrl-d,e",
|
||||
DetachKeys: "de",
|
||||
},
|
||||
},
|
||||
{
|
||||
options: withDefaultOpts(ExecOptions{
|
||||
Detach: true,
|
||||
DetachKeys: "ctrl-a,b",
|
||||
DetachKeys: "ab",
|
||||
}),
|
||||
configFile: configfile.ConfigFile{DetachKeys: "ctrl-d,e"},
|
||||
expected: client.ExecCreateOptions{
|
||||
configFile: configfile.ConfigFile{DetachKeys: "de"},
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
DetachKeys: "ctrl-a,b",
|
||||
DetachKeys: "ab",
|
||||
},
|
||||
},
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@@ -118,12 +118,12 @@ TWO=2
|
||||
},
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
_ = o.EnvFile.Set(tmpFile.Path())
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
{
|
||||
expected: client.ExecCreateOptions{
|
||||
expected: container.ExecOptions{
|
||||
Cmd: []string{"command"},
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
@@ -131,8 +131,8 @@ TWO=2
|
||||
},
|
||||
options: func() ExecOptions {
|
||||
o := withDefaultOpts(ExecOptions{})
|
||||
_ = o.EnvFile.Set(tmpFile.Path())
|
||||
_ = o.Env.Set("ONE=override")
|
||||
o.EnvFile.Set(tmpFile.Path())
|
||||
o.Env.Set("ONE=override")
|
||||
return o
|
||||
}(),
|
||||
},
|
||||
@@ -147,23 +147,13 @@ TWO=2
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseExecErrors(t *testing.T) {
|
||||
t.Run("missing env-file", func(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
assert.Check(t, execOpts.EnvFile.Set("no-such-env-file"))
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
assert.Check(t, execConfig == nil)
|
||||
})
|
||||
t.Run("invalid detach keys", func(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{
|
||||
DetachKeys: "shift-a",
|
||||
})
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.Check(t, is.ErrorContains(err, "invalid detach keys (shift-a):"))
|
||||
assert.Check(t, is.Nil(execConfig))
|
||||
})
|
||||
func TestParseExecNoSuchFile(t *testing.T) {
|
||||
execOpts := withDefaultOpts(ExecOptions{})
|
||||
execOpts.EnvFile.Set("no-such-env-file")
|
||||
execConfig, err := parseExec(execOpts, &configfile.ConfigFile{})
|
||||
assert.ErrorContains(t, err, "no-such-env-file")
|
||||
assert.Check(t, os.IsNotExist(err))
|
||||
assert.Check(t, execConfig == nil)
|
||||
}
|
||||
|
||||
func TestRunExec(t *testing.T) {
|
||||
@@ -186,8 +176,8 @@ func TestRunExec(t *testing.T) {
|
||||
doc: "inspect error",
|
||||
options: NewExecOptions(),
|
||||
client: &fakeClient{
|
||||
inspectFunc: func(string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("failed inspect")
|
||||
inspectFunc: func(string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("failed inspect")
|
||||
},
|
||||
},
|
||||
expectedError: "failed inspect",
|
||||
@@ -204,7 +194,7 @@ func TestRunExec(t *testing.T) {
|
||||
t.Run(testcase.doc, func(t *testing.T) {
|
||||
fakeCLI := test.NewFakeCli(testcase.client)
|
||||
|
||||
err := RunExec(context.TODO(), fakeCLI, "the-container", testcase.options)
|
||||
err := RunExec(context.TODO(), fakeCLI, "thecontainer", testcase.options)
|
||||
if testcase.expectedError != "" {
|
||||
assert.ErrorContains(t, err, testcase.expectedError)
|
||||
} else if !assert.Check(t, err) {
|
||||
@@ -216,8 +206,8 @@ func TestRunExec(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func execCreateWithID(_ string, _ client.ExecCreateOptions) (client.ExecCreateResult, error) {
|
||||
return client.ExecCreateResult{ID: "exec-id"}, nil
|
||||
func execCreateWithID(_ string, _ container.ExecOptions) (container.ExecCreateResponse, error) {
|
||||
return container.ExecCreateResponse{ID: "execid"}, nil
|
||||
}
|
||||
|
||||
func TestGetExecExitStatus(t *testing.T) {
|
||||
@@ -245,9 +235,9 @@ func TestGetExecExitStatus(t *testing.T) {
|
||||
|
||||
for _, testcase := range testcases {
|
||||
apiClient := &fakeClient{
|
||||
execInspectFunc: func(id string) (client.ExecInspectResult, error) {
|
||||
execInspectFunc: func(id string) (container.ExecInspect, error) {
|
||||
assert.Check(t, is.Equal(execID, id))
|
||||
return client.ExecInspectResult{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
return container.ExecInspect{ExitCode: testcase.exitCode}, testcase.inspectError
|
||||
},
|
||||
}
|
||||
err := getExecExitStatus(context.Background(), apiClient, execID)
|
||||
@@ -260,14 +250,14 @@ func TestNewExecCommandErrors(t *testing.T) {
|
||||
name string
|
||||
args []string
|
||||
expectedError string
|
||||
containerInspectFunc func(img string) (client.ContainerInspectResult, error)
|
||||
containerInspectFunc func(img string) (container.InspectResponse, error)
|
||||
}{
|
||||
{
|
||||
name: "client-error",
|
||||
args: []string{"5cb5bb5e4a3b", "-t", "-i", "bash"},
|
||||
expectedError: "something went wrong",
|
||||
containerInspectFunc: func(containerID string) (client.ContainerInspectResult, error) {
|
||||
return client.ContainerInspectResult{}, errors.New("something went wrong")
|
||||
containerInspectFunc: func(containerID string) (container.InspectResponse, error) {
|
||||
return container.InspectResponse{}, errors.New("something went wrong")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,15 +2,13 @@ package container
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/moby/sys/atomicwriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -19,7 +17,13 @@ type exportOptions struct {
|
||||
output string
|
||||
}
|
||||
|
||||
// newExportCommand creates a new "docker container export" command.
|
||||
// NewExportCommand creates a new `docker export` command
|
||||
//
|
||||
// Deprecated: Do not import commands directly. They will be removed in a future release.
|
||||
func NewExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
return newExportCommand(dockerCLI)
|
||||
}
|
||||
|
||||
func newExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
var opts exportOptions
|
||||
|
||||
@@ -34,8 +38,7 @@ func newExportCommand(dockerCLI command.Cli) *cobra.Command {
|
||||
Annotations: map[string]string{
|
||||
"aliases": "docker container export, docker export",
|
||||
},
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
DisableFlagsInUseLine: true,
|
||||
ValidArgsFunction: completion.ContainerNames(dockerCLI, true),
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
@@ -55,13 +58,13 @@ func runExport(ctx context.Context, dockerCLI command.Cli, opts exportOptions) e
|
||||
} else {
|
||||
writer, err := atomicwriter.New(opts.output, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to export container: %w", err)
|
||||
return errors.Wrap(err, "failed to export container")
|
||||
}
|
||||
defer writer.Close()
|
||||
output = writer
|
||||
}
|
||||
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container, client.ContainerExportOptions{})
|
||||
responseBody, err := dockerCLI.Client().ContainerExport(ctx, opts.container)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package container
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/moby/moby/client"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/fs"
|
||||
)
|
||||
@@ -15,9 +15,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
defer dir.Remove()
|
||||
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("bar"), nil
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("bar")), nil
|
||||
},
|
||||
})
|
||||
cmd := newExportCommand(cli)
|
||||
@@ -34,9 +33,8 @@ func TestContainerExportOutputToFile(t *testing.T) {
|
||||
|
||||
func TestContainerExportOutputToIrregularFile(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{
|
||||
containerExportFunc: func(container string) (client.ContainerExportResult, error) {
|
||||
// FIXME(thaJeztah): how to mock this?
|
||||
return mockContainerExportResult("foo"), nil
|
||||
containerExportFunc: func(container string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("foo")), nil
|
||||
},
|
||||
})
|
||||
cmd := newExportCommand(cli)
|
||||
|
||||
@@ -2,8 +2,7 @@ package container
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -13,6 +12,13 @@ const (
|
||||
pathHeader = "PATH"
|
||||
)
|
||||
|
||||
// NewDiffFormat returns a format for use with a diff Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func NewDiffFormat(source string) formatter.Format {
|
||||
return newDiffFormat(source)
|
||||
}
|
||||
|
||||
// newDiffFormat returns a format for use with a diff [formatter.Context].
|
||||
func newDiffFormat(source string) formatter.Format {
|
||||
if source == formatter.TableFormatKey {
|
||||
@@ -21,10 +27,17 @@ func newDiffFormat(source string) formatter.Format {
|
||||
return formatter.Format(source)
|
||||
}
|
||||
|
||||
// DiffFormatWrite writes formatted diff using the Context
|
||||
//
|
||||
// Deprecated: this function was only used internally and will be removed in the next release.
|
||||
func DiffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
||||
return diffFormatWrite(fmtCtx, changes)
|
||||
}
|
||||
|
||||
// diffFormatWrite writes formatted diff using the [formatter.Context].
|
||||
func diffFormatWrite(fmtCtx formatter.Context, changes client.ContainerDiffResult) error {
|
||||
func diffFormatWrite(fmtCtx formatter.Context, changes []container.FilesystemChange) error {
|
||||
return fmtCtx.Write(newDiffContext(), func(format func(subContext formatter.SubContext) error) error {
|
||||
for _, change := range changes.Changes {
|
||||
for _, change := range changes {
|
||||
if err := format(&diffContext{c: change}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user